84 lines
3.6 KiB
TypeScript
84 lines
3.6 KiB
TypeScript
// src/services/flyerDataTransformer.ts
|
|
import path from 'path';
|
|
import type { z } from 'zod';
|
|
import type { Logger } from 'pino';
|
|
import type { FlyerInsert, FlyerItemInsert } from '../types';
|
|
import type { AiFlyerDataSchema } from './flyerProcessingService.server';
|
|
import { generateFlyerIcon } from '../utils/imageProcessor';
|
|
|
|
/**
|
|
* This class is responsible for transforming the validated data from the AI service
|
|
* into the structured format required for database insertion (FlyerInsert and FlyerItemInsert).
|
|
*/
|
|
export class FlyerDataTransformer {
|
|
/**
|
|
* Transforms AI-extracted data into database-ready flyer and item records.
|
|
* @param extractedData The validated data from the AI.
|
|
* @param imagePaths The paths to the flyer images.
|
|
* @param originalFileName The original name of the uploaded file.
|
|
* @param checksum The checksum of the file.
|
|
* @param userId The ID of the user who uploaded the file, if any.
|
|
* @param logger The request-scoped or job-scoped logger instance.
|
|
* @returns A promise that resolves to an object containing the prepared flyer and item data.
|
|
*/
|
|
async transform(
|
|
extractedData: z.infer<typeof AiFlyerDataSchema>,
|
|
imagePaths: { path: string; mimetype: string }[],
|
|
originalFileName: string,
|
|
checksum: string,
|
|
userId: string | undefined,
|
|
logger: Logger,
|
|
): Promise<{ flyerData: FlyerInsert; itemsForDb: FlyerItemInsert[] }> {
|
|
logger.info('Starting data transformation from AI output to database format.');
|
|
|
|
const firstImage = imagePaths[0].path;
|
|
const iconFileName = await generateFlyerIcon(
|
|
firstImage,
|
|
path.join(path.dirname(firstImage), 'icons'),
|
|
logger,
|
|
);
|
|
|
|
const itemsForDb: FlyerItemInsert[] = extractedData.items.map((item) => ({
|
|
...item,
|
|
// Ensure 'item' is always a string, defaulting to 'Unknown Item' if null/undefined/empty.
|
|
item:
|
|
item.item === null || item.item === undefined || String(item.item).trim() === ''
|
|
? 'Unknown Item'
|
|
: String(item.item),
|
|
// Ensure 'price_display' is always a string, defaulting to empty if null/undefined.
|
|
price_display:
|
|
item.price_display === null || item.price_display === undefined
|
|
? ''
|
|
: String(item.price_display),
|
|
// Ensure 'quantity' is always a string, defaulting to empty if null/undefined.
|
|
quantity: item.quantity === null || item.quantity === undefined ? '' : String(item.quantity),
|
|
// Ensure 'category_name' is always a string, defaulting to 'Other/Miscellaneous' if null/undefined.
|
|
category_name: item.category_name === null || item.category_name === undefined ? 'Other/Miscellaneous' : String(item.category_name),
|
|
master_item_id: item.master_item_id === null ? undefined : item.master_item_id, // Convert null to undefined
|
|
view_count: 0,
|
|
click_count: 0,
|
|
updated_at: new Date().toISOString(),
|
|
}));
|
|
|
|
const flyerData: FlyerInsert = {
|
|
file_name: originalFileName,
|
|
image_url: `/flyer-images/${path.basename(firstImage)}`,
|
|
icon_url: `/flyer-images/icons/${iconFileName}`,
|
|
checksum,
|
|
store_name: extractedData.store_name || 'Unknown Store (auto)',
|
|
valid_from: extractedData.valid_from,
|
|
valid_to: extractedData.valid_to,
|
|
store_address: extractedData.store_address, // The number of items is now calculated directly from the transformed data.
|
|
item_count: itemsForDb.length,
|
|
uploaded_by: userId,
|
|
};
|
|
|
|
logger.info(
|
|
{ itemCount: itemsForDb.length, storeName: flyerData.store_name },
|
|
'Data transformation complete.',
|
|
);
|
|
|
|
return { flyerData, itemsForDb };
|
|
}
|
|
}
|