// src/services/db/flyerLocation.db.ts /** * Repository for managing flyer_locations (many-to-many relationship between flyers and store_locations). */ import type { Logger } from 'pino'; import type { PoolClient, Pool } from 'pg'; import { handleDbError } from './errors.db'; import type { FlyerLocation } from '../../types'; import { getPool } from './connection.db'; export class FlyerLocationRepository { private db: Pool | PoolClient; constructor(dbClient?: PoolClient) { this.db = dbClient || getPool(); } /** * Links a flyer to one or more store locations. * @param flyerId The ID of the flyer * @param storeLocationIds Array of store_location_ids to associate with this flyer * @param logger Logger instance * @returns Promise that resolves when all links are created */ async linkFlyerToLocations( flyerId: number, storeLocationIds: number[], logger: Logger, ): Promise { try { if (storeLocationIds.length === 0) { logger.warn({ flyerId }, 'No store locations provided for flyer linkage'); return; } // Use VALUES with multiple rows for efficient bulk insert const values = storeLocationIds.map((_, index) => `($1, $${index + 2})`).join(', '); const query = ` INSERT INTO public.flyer_locations (flyer_id, store_location_id) VALUES ${values} ON CONFLICT (flyer_id, store_location_id) DO NOTHING `; await this.db.query(query, [flyerId, ...storeLocationIds]); logger.info( { flyerId, locationCount: storeLocationIds.length }, 'Linked flyer to store locations', ); } catch (error) { handleDbError( error, logger, 'Database error in linkFlyerToLocations', { flyerId, storeLocationIds }, { defaultMessage: 'Failed to link flyer to store locations.', }, ); } } /** * Links a flyer to all locations of a given store. * This is a convenience method for the common case where a flyer is valid at all store locations. * @param flyerId The ID of the flyer * @param storeId The ID of the store * @param logger Logger instance * @returns Promise that resolves to the number of locations linked */ async linkFlyerToAllStoreLocations( flyerId: number, storeId: number, logger: Logger, ): Promise { try { const query = ` INSERT INTO public.flyer_locations (flyer_id, store_location_id) SELECT $1, store_location_id FROM public.store_locations WHERE store_id = $2 ON CONFLICT (flyer_id, store_location_id) DO NOTHING RETURNING store_location_id `; const res = await this.db.query(query, [flyerId, storeId]); const linkedCount = res.rowCount || 0; logger.info({ flyerId, storeId, linkedCount }, 'Linked flyer to all store locations'); return linkedCount; } catch (error) { handleDbError( error, logger, 'Database error in linkFlyerToAllStoreLocations', { flyerId, storeId }, { defaultMessage: 'Failed to link flyer to all store locations.', }, ); } } /** * Removes all location links for a flyer. * @param flyerId The ID of the flyer * @param logger Logger instance */ async unlinkAllLocations(flyerId: number, logger: Logger): Promise { try { await this.db.query('DELETE FROM public.flyer_locations WHERE flyer_id = $1', [flyerId]); logger.info({ flyerId }, 'Unlinked all locations from flyer'); } catch (error) { handleDbError( error, logger, 'Database error in unlinkAllLocations', { flyerId }, { defaultMessage: 'Failed to unlink locations from flyer.', }, ); } } /** * Removes a specific location link from a flyer. * @param flyerId The ID of the flyer * @param storeLocationId The ID of the store location to unlink * @param logger Logger instance */ async unlinkLocation(flyerId: number, storeLocationId: number, logger: Logger): Promise { try { await this.db.query( 'DELETE FROM public.flyer_locations WHERE flyer_id = $1 AND store_location_id = $2', [flyerId, storeLocationId], ); logger.info({ flyerId, storeLocationId }, 'Unlinked location from flyer'); } catch (error) { handleDbError( error, logger, 'Database error in unlinkLocation', { flyerId, storeLocationId }, { defaultMessage: 'Failed to unlink location from flyer.', }, ); } } /** * Gets all location IDs associated with a flyer. * @param flyerId The ID of the flyer * @param logger Logger instance * @returns Promise that resolves to an array of store_location_ids */ async getLocationIdsByFlyerId(flyerId: number, logger: Logger): Promise { try { const res = await this.db.query<{ store_location_id: number }>( 'SELECT store_location_id FROM public.flyer_locations WHERE flyer_id = $1', [flyerId], ); return res.rows.map((row) => row.store_location_id); } catch (error) { handleDbError( error, logger, 'Database error in getLocationIdsByFlyerId', { flyerId }, { defaultMessage: 'Failed to get location IDs for flyer.', }, ); } } /** * Gets all flyer_location records for a flyer. * @param flyerId The ID of the flyer * @param logger Logger instance * @returns Promise that resolves to an array of FlyerLocation objects */ async getFlyerLocationsByFlyerId(flyerId: number, logger: Logger): Promise { try { const res = await this.db.query( 'SELECT * FROM public.flyer_locations WHERE flyer_id = $1', [flyerId], ); return res.rows; } catch (error) { handleDbError( error, logger, 'Database error in getFlyerLocationsByFlyerId', { flyerId }, { defaultMessage: 'Failed to get flyer locations.', }, ); } } }