All files / src/services flyerPersistenceService.server.ts

97.7% Statements 85/87
84.61% Branches 11/13
88.88% Functions 8/9
97.29% Lines 72/74

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 752x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 59x 2x 2x 2x 2x 2x 2x 2x 2x 2x 4x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x     4x 4x 2x 3x 2x 2x 2x 2x 3x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 3x 2x 2x 2x 3x 2x 2x 2x 3x 2x 2x  
// src/services/flyerPersistenceService.server.ts
import type { Logger } from 'pino';
import type { PoolClient } from 'pg';
import { withTransaction as defaultWithTransaction } from './db/connection.db';
import { createFlyerAndItems } from './db/flyer.db';
import { AdminRepository } from './db/admin.db';
import { GamificationRepository } from './db/gamification.db';
import { cacheService } from './cacheService.server';
import type { FlyerInsert, FlyerItemInsert, Flyer } from '../types';
 
export type WithTransactionFn = <T>(callback: (client: PoolClient) => Promise<T>) => Promise<T>;
 
export class FlyerPersistenceService {
  private withTransaction: WithTransactionFn;
 
  constructor(withTransactionFn: WithTransactionFn = defaultWithTransaction) {
    this.withTransaction = withTransactionFn;
  }
 
  /**
   * Allows replacing the withTransaction function at runtime.
   * This is primarily used for testing to inject mock implementations.
   * Pass null to reset to the default implementation.
   * @internal
   */
  _setWithTransaction(fn: WithTransactionFn | null): void {
    this.withTransaction = fn ?? defaultWithTransaction;
  }
 
  /**
   * Saves the flyer and its items to the database within a transaction.
   * Also logs the activity and invalidates related cache entries.
   */
  async saveFlyer(
    flyerData: FlyerInsert,
    itemsForDb: FlyerItemInsert[],
    userId: string | undefined,
    logger: Logger,
  ): Promise<Flyer> {
    const flyer = await this.withTransaction(async (client) => {
      const { flyer, items } = await createFlyerAndItems(flyerData, itemsForDb, logger, client);
 
      logger.info(
        `Successfully processed flyer: ${flyer.file_name} (ID: ${flyer.flyer_id}) with ${items.length} items.`,
      );
 
      // Log activity if a user uploaded it
      if (userId) {
        const transactionalAdminRepo = new AdminRepository(client);
        await transactionalAdminRepo.logActivity(
          {
            userId: userId,
            action: 'flyer_processed',
            displayText: `Processed a new flyer for ${flyerData.store_name}.`,
            details: { flyerId: flyer.flyer_id, storeName: flyerData.store_name },
          },
          logger,
        );
 
        // Award 'First-Upload' achievement
        const gamificationRepo = new GamificationRepository(client);
        await gamificationRepo.awardAchievement(userId, 'First-Upload', logger);
      }
      return flyer;
    });
 
    // Invalidate flyer list cache after successful creation (fire-and-forget)
    cacheService.invalidateFlyers(logger).catch(() => {
      // Error already logged in invalidateFlyers
    });
 
    return flyer;
  }
}