// src/services/db/flyer.ts import { getPool } from './connection'; import { logger } from '../logger'; import { Flyer, Brand, MasterGroceryItem, FlyerItem } from '../../types'; /** * Retrieves all flyers from the database, joining with store information. * @returns A promise that resolves to an array of Flyer objects. */ // prettier-ignore export async function getFlyers(): Promise { try { const query = ` SELECT f.flyer_id, f.created_at, f.file_name, f.image_url, f.checksum, f.store_id, f.valid_from, f.valid_to, f.store_address, json_build_object( 'store_id', s.store_id, 'name', s.name, 'logo_url', s.logo_url ) as store FROM public.flyers f LEFT JOIN public.stores s ON f.store_id = s.store_id ORDER BY f.valid_to DESC, s.name ASC; `; const res = await getPool().query(query); return res.rows; } catch (error) { logger.error('Database error in getFlyers:', { error }); throw new Error('Failed to retrieve flyers from database.'); } } /** * Retrieves all brands from the database, including the associated store name for store brands. * @returns A promise that resolves to an array of Brand objects. */ // prettier-ignore export async function getAllBrands(): Promise { try { const query = ` SELECT b.brand_id, b.name, b.logo_url, b.store_id, s.name as store_name FROM public.brands b LEFT JOIN public.stores s ON b.store_id = s.store_id ORDER BY b.name ASC; `; const res = await getPool().query(query); return res.rows; } catch (error) { logger.error('Database error in getAllBrands:', { error }); throw new Error('Failed to retrieve brands from database.'); } } /** * Retrieves all master grocery items from the database, joining with category information. * @returns A promise that resolves to an array of MasterGroceryItem objects. */ // prettier-ignore export async function getAllMasterItems(): Promise { try { const query = ` SELECT m.master_grocery_item_id, m.created_at, m.name, m.category_id, c.name as category_name FROM public.master_grocery_items m LEFT JOIN public.categories c ON m.category_id = c.category_id ORDER BY m.name ASC; `; const res = await getPool().query(query); return res.rows; } catch (error) { logger.error('Database error in getAllMasterItems:', { error }); throw new Error('Failed to retrieve master items from database.'); } } /** * Retrieves all categories from the database. * @returns A promise that resolves to an array of Category objects. */ // prettier-ignore export async function getAllCategories(): Promise<{category_id: number, name: string}[]> { try { const query = ` SELECT category_id, name FROM public.categories ORDER BY name ASC; `; const res = await getPool().query<{category_id: number, name: string}>(query); return res.rows; } catch (error) { logger.error('Database error in getAllCategories:', { error }); throw new Error('Failed to retrieve categories from database.'); } } /** * Finds a flyer by its checksum to prevent duplicate processing. * @param checksum The SHA-256 checksum of the flyer file. * @returns A promise that resolves to the Flyer object if found, otherwise undefined. */ // prettier-ignore export async function findFlyerByChecksum(checksum: string): Promise { try { const res = await getPool().query('SELECT * FROM public.flyers WHERE checksum = $1', [checksum]); return res.rows[0]; } catch (error) { logger.error('Database error in findFlyerByChecksum:', { error }); throw new Error('Failed to check for existing flyer.'); } } /** * Creates a new flyer and all its associated items in a single database transaction. * @param flyerData The metadata for the flyer. * @param items The array of flyer items extracted from the flyer. * @returns A promise that resolves to the newly created Flyer object. */ // prettier-ignore export async function createFlyerAndItems( flyerData: Omit & { store_name: string }, items: Omit[] ): Promise { const client = await getPool().connect(); logger.debug('[DB createFlyerAndItems] Starting transaction to create flyer.', { flyerData: { name: flyerData.file_name, store_name: flyerData.store_name }, itemCount: items.length }); try { await client.query('BEGIN'); logger.debug('[DB createFlyerAndItems] BEGIN transaction successful.'); // Find or create the store to get its ID. This logic is now self-contained. let storeId: number; const storeRes = await client.query<{ store_id: number }>('SELECT store_id FROM public.stores WHERE name = $1', [flyerData.store_name]); if (storeRes.rows.length > 0) { storeId = storeRes.rows[0].store_id; } else { const newStoreRes = await client.query<{ store_id: number }>('INSERT INTO public.stores (name) VALUES ($1) RETURNING store_id', [flyerData.store_name]); storeId = newStoreRes.rows[0].store_id; } // Create the flyer record const flyerQuery = ` INSERT INTO public.flyers (file_name, image_url, checksum, store_id, valid_from, valid_to, store_address, uploaded_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; `; const flyerValues = [flyerData.file_name, flyerData.image_url, flyerData.checksum, storeId, flyerData.valid_from, flyerData.valid_to, flyerData.store_address, flyerData.uploaded_by]; const newFlyerRes = await client.query(flyerQuery, flyerValues); const newFlyer = newFlyerRes.rows[0]; // Prepare and insert all flyer items if (items.length > 0) { const itemInsertQuery = ` INSERT INTO public.flyer_items ( flyer_id, item, price_display, price_in_cents, quantity, master_item_id, -- This will be populated by our suggestion function category_name, unit_price ) VALUES ($1, $2, $3, $4, $5, public.suggest_master_item_for_flyer_item($2), $6, $7) `; // Loop through each item and execute the insert query. // The query now directly calls the `suggest_master_item_for_flyer_item` function // on the database side, passing the item name (`item.item`) as the argument. // This is more efficient than making a separate DB call for each item to get the suggestion. for (const item of items) { const itemValues = [ newFlyer.flyer_id, item.item, item.price_display, item.price_in_cents, item.quantity, item.category_name, item.unit_price ? JSON.stringify(item.unit_price) : null // Ensure JSONB is correctly stringified ]; await client.query(itemInsertQuery, itemValues); } } await client.query('COMMIT'); logger.debug(`[DB createFlyerAndItems] COMMIT transaction successful for new flyer ID: ${newFlyer.flyer_id}.`); return newFlyer; } catch (error) { await client.query('ROLLBACK'); logger.error('[DB createFlyerAndItems] ROLLBACK transaction due to error.'); logger.error('Database transaction error in createFlyerAndItems:', { error }); throw new Error('Failed to save flyer and its items.'); } finally { client.release(); } } /** * Retrieves all items for a specific flyer. * @param flyerId The ID of the flyer. * @returns A promise that resolves to an array of FlyerItem objects. */ // prettier-ignore export async function getFlyerItems(flyerId: number): Promise { try { const query = ` SELECT * FROM public.flyer_items WHERE flyer_id = $1 ORDER BY flyer_item_id ASC; `; const res = await getPool().query(query, [flyerId]); return res.rows; } catch (error) { logger.error('Database error in getFlyerItems:', { error, flyerId }); throw new Error('Failed to retrieve flyer items.'); } } /** * Retrieves all flyer items for a given list of flyer IDs. * @param flyerIds An array of flyer IDs. * @returns A promise that resolves to an array of FlyerItem objects. */ // prettier-ignore export async function getFlyerItemsForFlyers(flyerIds: number[]): Promise { try { const query = ` SELECT * FROM public.flyer_items WHERE flyer_id = ANY($1::bigint[]); `; const res = await getPool().query(query, [flyerIds]); return res.rows; } catch (error) { logger.error('Database error in getFlyerItemsForFlyers:', { error }); throw new Error('Failed to retrieve items for multiple flyers.'); } } /** * Counts the total number of flyer items for a given list of flyer IDs. * @param flyerIds An array of flyer IDs. * @returns A promise that resolves to the total count of items. */ // prettier-ignore export async function countFlyerItemsForFlyers(flyerIds: number[]): Promise { try { const query = `SELECT COUNT(*) FROM public.flyer_items WHERE flyer_id = ANY($1::bigint[])`; const res = await getPool().query<{ count: string }>(query, [flyerIds]); return parseInt(res.rows[0].count, 10); } catch (error) { logger.error('Database error in countFlyerItemsForFlyers:', { error }); throw new Error('Failed to count items for multiple flyers.'); } } /** * Updates the logo URL for a specific store. * @param storeId The ID of the store to update. * @param logoUrl The new URL for the store's logo. */ // prettier-ignore export async function updateStoreLogo(storeId: number, logoUrl: string): Promise { try { await getPool().query( 'UPDATE public.stores SET logo_url = $1 WHERE id = $2', [logoUrl, storeId] ); } catch (error) { logger.error('Database error in updateStoreLogo:', { error, storeId }); throw new Error('Failed to update store logo in database.'); } } /** * Tracks a user interaction with a flyer item (view or click). * @param itemId The ID of the flyer item. * @param type The type of interaction ('view' or 'click'). */ export async function trackFlyerItemInteraction(itemId: number, type: 'view' | 'click'): Promise { try { const column = type === 'view' ? 'view_count' : 'click_count'; // Use the || operator to concatenate the column name safely into the query. const query = `UPDATE public.flyer_items SET ${column} = ${column} + 1 WHERE flyer_item_id = $1`; await getPool().query(query, [itemId]); } catch (error) { logger.error('Database error in trackFlyerItemInteraction:', { error, itemId, type }); // This is a non-critical operation, so we don't throw an error that would crash the user's request. } } /** * Retrieves historical price data for a given list of master item IDs. * This function queries the pre-aggregated `item_price_history` table for efficiency. * @param masterItemIds An array of master grocery item IDs. * @returns A promise that resolves to an array of historical price records. */ // prettier-ignore export async function getHistoricalPriceDataForItems(masterItemIds: number[]): Promise<{ master_item_id: number; summary_date: string; avg_price_in_cents: number | null; }[]> { if (masterItemIds.length === 0) { return []; } try { const query = ` SELECT master_item_id, summary_date, avg_price_in_cents FROM public.item_price_history WHERE master_item_id = ANY($1::bigint[]) ORDER BY summary_date ASC; `; const res = await getPool().query(query, [masterItemIds]); return res.rows; } catch (error) { logger.error('Database error in getHistoricalPriceDataForItems:', { error }); throw new Error('Failed to retrieve historical price data.'); } }