Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
622c919733 | ||
| c7f6b6369a | |||
|
|
879d956003 | ||
| 27eaac7ea8 | |||
|
|
93618c57e5 | ||
| 7f043ef704 | |||
|
|
62e35deddc | ||
| 59f6f43d03 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.9.48",
|
"version": "0.9.52",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"version": "0.9.48",
|
"version": "0.9.52",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^6.14.2",
|
"@bull-board/api": "^6.14.2",
|
||||||
"@bull-board/express": "^6.14.2",
|
"@bull-board/express": "^6.14.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "flyer-crawler",
|
"name": "flyer-crawler",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.48",
|
"version": "0.9.52",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const uploadAndProcessFlyer = async (
|
|||||||
formData.append('checksum', checksum);
|
formData.append('checksum', checksum);
|
||||||
|
|
||||||
logger.info(`[aiApiClient] Starting background processing for file: ${file.name}`);
|
logger.info(`[aiApiClient] Starting background processing for file: ${file.name}`);
|
||||||
|
console.error(`[aiApiClient] uploadAndProcessFlyer: Uploading file '${file.name}' with checksum '${checksum}'`);
|
||||||
|
|
||||||
const response = await authedPostForm('/ai/upload-and-process', formData, { tokenOverride });
|
const response = await authedPostForm('/ai/upload-and-process', formData, { tokenOverride });
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ export const getJobStatus = async (
|
|||||||
jobId: string,
|
jobId: string,
|
||||||
tokenOverride?: string,
|
tokenOverride?: string,
|
||||||
): Promise<JobStatus> => {
|
): Promise<JobStatus> => {
|
||||||
|
console.error(`[aiApiClient] getJobStatus: Fetching status for job '${jobId}'`);
|
||||||
const response = await authedGet(`/ai/jobs/${jobId}/status`, { tokenOverride });
|
const response = await authedGet(`/ai/jobs/${jobId}/status`, { tokenOverride });
|
||||||
|
|
||||||
// Handle non-OK responses first, as they might not have a JSON body.
|
// Handle non-OK responses first, as they might not have a JSON body.
|
||||||
|
|||||||
@@ -328,9 +328,8 @@ describe('AI Service (Server)', () => {
|
|||||||
// Check that a warning was logged
|
// Check that a warning was logged
|
||||||
expect(logger.warn).toHaveBeenCalledWith(
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
// The warning should be for the model that failed ('gemini-2.5-flash'), not the next one.
|
// The warning should be for the model that failed ('gemini-2.5-flash'), not the next one.
|
||||||
// The warning should be for the model that failed, not the next one.
|
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
`Model '${models[0]}' failed due to quota/rate limit. Trying next model.`,
|
`Model '${models[0]}' failed due to quota/rate limit/overload. Trying next model.`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -506,7 +505,7 @@ describe('AI Service (Server)', () => {
|
|||||||
expect(mockGenerateContent).toHaveBeenCalledTimes(2);
|
expect(mockGenerateContent).toHaveBeenCalledTimes(2);
|
||||||
expect(mockGenerateContent).toHaveBeenNthCalledWith(1, { model: models[0], ...request });
|
expect(mockGenerateContent).toHaveBeenNthCalledWith(1, { model: models[0], ...request });
|
||||||
expect(mockGenerateContent).toHaveBeenNthCalledWith(2, { model: models[1], ...request });
|
expect(mockGenerateContent).toHaveBeenNthCalledWith(2, { model: models[1], ...request });
|
||||||
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining(`Model '${models[0]}' failed due to quota/rate limit.`));
|
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining(`Model '${models[0]}' failed due to quota/rate limit/overload.`));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail immediately on a 400 Bad Request error without retrying', async () => {
|
it('should fail immediately on a 400 Bad Request error without retrying', async () => {
|
||||||
|
|||||||
@@ -543,6 +543,20 @@ export class AIService {
|
|||||||
logger.info(
|
logger.info(
|
||||||
`[extractCoreDataFromFlyerImage] Entering method with ${imagePaths.length} image(s).`,
|
`[extractCoreDataFromFlyerImage] Entering method with ${imagePaths.length} image(s).`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// [TEST HOOK] Simulate an AI failure if the filename contains specific text.
|
||||||
|
// This allows integration tests to verify error handling.
|
||||||
|
if (imagePaths.some((f) => f.path.includes('ai-fail-test'))) {
|
||||||
|
logger.warn('[TEST HOOK] Simulating AI failure for test file.');
|
||||||
|
throw new Error('AI model failed to extract data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// [TEST HOOK] Simulate a specific failure for the cleanup test
|
||||||
|
if (imagePaths.some((f) => f.path.includes('cleanup-fail-test'))) {
|
||||||
|
logger.warn('[TEST HOOK] Simulating AI failure for cleanup test.');
|
||||||
|
throw new Error('Simulated AI failure for cleanup test.');
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = this._buildFlyerExtractionPrompt(masterItems, submitterIp, userProfileAddress);
|
const prompt = this._buildFlyerExtractionPrompt(masterItems, submitterIp, userProfileAddress);
|
||||||
|
|
||||||
const imageParts = await Promise.all(
|
const imageParts = await Promise.all(
|
||||||
@@ -796,6 +810,7 @@ async enqueueFlyerProcessing(
|
|||||||
|
|
||||||
const baseUrl = baseUrlOverride || getBaseUrl(logger);
|
const baseUrl = baseUrlOverride || getBaseUrl(logger);
|
||||||
// --- START DEBUGGING ---
|
// --- START DEBUGGING ---
|
||||||
|
console.error(`[DEBUG] aiService.enqueueFlyerProcessing resolved baseUrl: "${baseUrl}"`);
|
||||||
// Add a fail-fast check to ensure the baseUrl is a valid URL before enqueuing.
|
// Add a fail-fast check to ensure the baseUrl is a valid URL before enqueuing.
|
||||||
// This will make the test fail at the upload step if the URL is the problem,
|
// This will make the test fail at the upload step if the URL is the problem,
|
||||||
// which is easier to debug than a worker failure.
|
// which is easier to debug than a worker failure.
|
||||||
|
|||||||
@@ -98,9 +98,12 @@ describe('AnalyticsService', () => {
|
|||||||
|
|
||||||
const promise = service.processDailyReportJob(job);
|
const promise = service.processDailyReportJob(job);
|
||||||
|
|
||||||
|
// Capture the expectation promise BEFORE triggering the rejection via timer advancement.
|
||||||
|
const expectation = expect(promise).rejects.toThrow('A string error');
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(10000);
|
await vi.advanceTimersByTimeAsync(10000);
|
||||||
|
|
||||||
await expect(promise).rejects.toThrow('A string error');
|
await expectation;
|
||||||
|
|
||||||
expect(mockLoggerInstance.error).toHaveBeenCalledWith(
|
expect(mockLoggerInstance.error).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -188,9 +191,12 @@ describe('AnalyticsService', () => {
|
|||||||
|
|
||||||
const promise = service.processWeeklyReportJob(job);
|
const promise = service.processWeeklyReportJob(job);
|
||||||
|
|
||||||
|
// Capture the expectation promise BEFORE triggering the rejection via timer advancement.
|
||||||
|
const expectation = expect(promise).rejects.toThrow('A string error');
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(30000);
|
await vi.advanceTimersByTimeAsync(30000);
|
||||||
|
|
||||||
await expect(promise).rejects.toThrow('A string error');
|
await expectation;
|
||||||
|
|
||||||
expect(mockLoggerInstance.error).toHaveBeenCalledWith(
|
expect(mockLoggerInstance.error).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const apiFetch = async (
|
|||||||
const fullUrl = url.startsWith('http') ? url : joinUrl(API_BASE_URL, url);
|
const fullUrl = url.startsWith('http') ? url : joinUrl(API_BASE_URL, url);
|
||||||
|
|
||||||
logger.debug(`apiFetch: ${options.method || 'GET'} ${fullUrl}`);
|
logger.debug(`apiFetch: ${options.method || 'GET'} ${fullUrl}`);
|
||||||
|
console.error(`[apiClient] apiFetch Request: ${options.method || 'GET'} ${fullUrl}`);
|
||||||
|
|
||||||
// Create a new headers object to avoid mutating the original options.
|
// Create a new headers object to avoid mutating the original options.
|
||||||
const headers = new Headers(options.headers || {});
|
const headers = new Headers(options.headers || {});
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ export class BackgroundJobService {
|
|||||||
const reportDate = getCurrentDateISOString(); // YYYY-MM-DD
|
const reportDate = getCurrentDateISOString(); // YYYY-MM-DD
|
||||||
const jobId = `manual-report-${reportDate}-${Date.now()}`;
|
const jobId = `manual-report-${reportDate}-${Date.now()}`;
|
||||||
const job = await analyticsQueue.add('generate-daily-report', { reportDate }, { jobId });
|
const job = await analyticsQueue.add('generate-daily-report', { reportDate }, { jobId });
|
||||||
return job.id!;
|
if (!job.id) {
|
||||||
|
throw new Error('Failed to enqueue daily report job: No job ID returned');
|
||||||
|
}
|
||||||
|
return job.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async triggerWeeklyAnalyticsReport(): Promise<string> {
|
public async triggerWeeklyAnalyticsReport(): Promise<string> {
|
||||||
@@ -45,7 +48,10 @@ export class BackgroundJobService {
|
|||||||
{ reportYear, reportWeek },
|
{ reportYear, reportWeek },
|
||||||
{ jobId },
|
{ jobId },
|
||||||
);
|
);
|
||||||
return job.id!;
|
if (!job.id) {
|
||||||
|
throw new Error('Failed to enqueue weekly report job: No job ID returned');
|
||||||
|
}
|
||||||
|
return job.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -64,7 +64,17 @@ export class FlyerRepository {
|
|||||||
*/
|
*/
|
||||||
async insertFlyer(flyerData: FlyerDbInsert, logger: Logger): Promise<Flyer> {
|
async insertFlyer(flyerData: FlyerDbInsert, logger: Logger): Promise<Flyer> {
|
||||||
console.error('[DEBUG] FlyerRepository.insertFlyer called with:', JSON.stringify(flyerData, null, 2));
|
console.error('[DEBUG] FlyerRepository.insertFlyer called with:', JSON.stringify(flyerData, null, 2));
|
||||||
|
// [TEST HOOK] Simulate a database failure if the filename contains specific text.
|
||||||
|
// This allows integration tests to verify error handling without mocking the entire DB connection.
|
||||||
|
if (flyerData.file_name.includes('db-fail-test')) {
|
||||||
|
logger.warn('[TEST HOOK] Simulating DB transaction failure for test file.');
|
||||||
|
throw new Error('DB transaction failed for test.');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Sanitize icon_url: Ensure empty strings become NULL to avoid regex constraint violations
|
||||||
|
const iconUrl = flyerData.icon_url && flyerData.icon_url.trim() !== '' ? flyerData.icon_url : null;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO flyers (
|
INSERT INTO flyers (
|
||||||
file_name, image_url, icon_url, checksum, store_id, valid_from, valid_to, store_address,
|
file_name, image_url, icon_url, checksum, store_id, valid_from, valid_to, store_address,
|
||||||
@@ -76,7 +86,7 @@ export class FlyerRepository {
|
|||||||
const values = [
|
const values = [
|
||||||
flyerData.file_name, // $1
|
flyerData.file_name, // $1
|
||||||
flyerData.image_url, // $2
|
flyerData.image_url, // $2
|
||||||
flyerData.icon_url, // $3
|
iconUrl, // $3
|
||||||
flyerData.checksum, // $4
|
flyerData.checksum, // $4
|
||||||
flyerData.store_id, // $5
|
flyerData.store_id, // $5
|
||||||
flyerData.valid_from, // $6
|
flyerData.valid_from, // $6
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export class FlyerAiProcessor {
|
|||||||
jobData: FlyerJobData,
|
jobData: FlyerJobData,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<AiProcessorResult> {
|
): Promise<AiProcessorResult> {
|
||||||
|
console.error(`[WORKER DEBUG] FlyerAiProcessor: extractAndValidateData called with ${imagePaths.length} images`);
|
||||||
logger.info(`Starting AI data extraction for ${imagePaths.length} pages.`);
|
logger.info(`Starting AI data extraction for ${imagePaths.length} pages.`);
|
||||||
const { submitterIp, userProfileAddress } = jobData;
|
const { submitterIp, userProfileAddress } = jobData;
|
||||||
const masterItems = await this.personalizationRepo.getAllMasterItems(logger);
|
const masterItems = await this.personalizationRepo.getAllMasterItems(logger);
|
||||||
@@ -159,6 +160,7 @@ export class FlyerAiProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Batch processing complete. Total items extracted: ${mergedData.items.length}`);
|
logger.info(`Batch processing complete. Total items extracted: ${mergedData.items.length}`);
|
||||||
|
console.error(`[WORKER DEBUG] FlyerAiProcessor: Merged AI Data:`, JSON.stringify(mergedData, null, 2));
|
||||||
|
|
||||||
// Validate the final merged dataset
|
// Validate the final merged dataset
|
||||||
return this._validateAiData(mergedData, logger);
|
return this._validateAiData(mergedData, logger);
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ export class FlyerDataTransformer {
|
|||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): { imageUrl: string; iconUrl: string } {
|
): { imageUrl: string; iconUrl: string } {
|
||||||
console.log('[DEBUG] FlyerDataTransformer._buildUrls inputs:', { imageFileName, iconFileName, baseUrl });
|
console.error('[DEBUG] FlyerDataTransformer._buildUrls inputs:', { imageFileName, iconFileName, baseUrl });
|
||||||
logger.debug({ imageFileName, iconFileName, baseUrl }, 'Building URLs');
|
logger.debug({ imageFileName, iconFileName, baseUrl }, 'Building URLs');
|
||||||
const finalBaseUrl = baseUrl || getBaseUrl(logger);
|
const finalBaseUrl = baseUrl || getBaseUrl(logger);
|
||||||
console.log('[DEBUG] FlyerDataTransformer._buildUrls finalBaseUrl resolved to:', finalBaseUrl);
|
console.error('[DEBUG] FlyerDataTransformer._buildUrls finalBaseUrl resolved to:', finalBaseUrl);
|
||||||
const imageUrl = `${finalBaseUrl}/flyer-images/${imageFileName}`;
|
const imageUrl = `${finalBaseUrl}/flyer-images/${imageFileName}`;
|
||||||
const iconUrl = `${finalBaseUrl}/flyer-images/icons/${iconFileName}`;
|
const iconUrl = `${finalBaseUrl}/flyer-images/icons/${iconFileName}`;
|
||||||
console.log('[DEBUG] FlyerDataTransformer._buildUrls constructed:', { imageUrl, iconUrl });
|
console.error('[DEBUG] FlyerDataTransformer._buildUrls constructed:', { imageUrl, iconUrl });
|
||||||
logger.debug({ imageUrl, iconUrl }, 'Constructed URLs');
|
logger.debug({ imageUrl, iconUrl }, 'Constructed URLs');
|
||||||
return { imageUrl, iconUrl };
|
return { imageUrl, iconUrl };
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ export class FlyerDataTransformer {
|
|||||||
logger: Logger,
|
logger: Logger,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
): Promise<{ flyerData: FlyerInsert; itemsForDb: FlyerItemInsert[] }> {
|
): Promise<{ flyerData: FlyerInsert; itemsForDb: FlyerItemInsert[] }> {
|
||||||
console.log('[DEBUG] FlyerDataTransformer.transform called with baseUrl:', baseUrl);
|
console.error('[DEBUG] FlyerDataTransformer.transform called with baseUrl:', baseUrl);
|
||||||
logger.info('Starting data transformation from AI output to database format.');
|
logger.info('Starting data transformation from AI output to database format.');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -253,7 +253,9 @@ export class FlyerFileHandler {
|
|||||||
job: Job<FlyerJobData>,
|
job: Job<FlyerJobData>,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<{ imagePaths: { path: string; mimetype: string }[]; createdImagePaths: string[] }> {
|
): Promise<{ imagePaths: { path: string; mimetype: string }[]; createdImagePaths: string[] }> {
|
||||||
|
console.error(`[WORKER DEBUG] FlyerFileHandler: prepareImageInputs called for ${filePath}`);
|
||||||
const fileExt = path.extname(filePath).toLowerCase();
|
const fileExt = path.extname(filePath).toLowerCase();
|
||||||
|
console.error(`[WORKER DEBUG] FlyerFileHandler: Detected extension: ${fileExt}`);
|
||||||
|
|
||||||
if (fileExt === '.pdf') {
|
if (fileExt === '.pdf') {
|
||||||
return this._handlePdfInput(filePath, job, logger);
|
return this._handlePdfInput(filePath, job, logger);
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export class FlyerProcessingService {
|
|||||||
// Stage 1: Prepare Inputs (e.g., convert PDF to images)
|
// Stage 1: Prepare Inputs (e.g., convert PDF to images)
|
||||||
stages[0].status = 'in-progress';
|
stages[0].status = 'in-progress';
|
||||||
await job.updateProgress({ stages });
|
await job.updateProgress({ stages });
|
||||||
|
console.error(`[WORKER DEBUG] ProcessingService: Calling fileHandler.prepareImageInputs for ${job.data.filePath}`);
|
||||||
|
|
||||||
const { imagePaths, createdImagePaths } = await this.fileHandler.prepareImageInputs(
|
const { imagePaths, createdImagePaths } = await this.fileHandler.prepareImageInputs(
|
||||||
job.data.filePath,
|
job.data.filePath,
|
||||||
@@ -76,6 +77,7 @@ export class FlyerProcessingService {
|
|||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
allFilePaths.push(...createdImagePaths);
|
allFilePaths.push(...createdImagePaths);
|
||||||
|
console.error(`[WORKER DEBUG] ProcessingService: fileHandler returned ${imagePaths.length} images.`);
|
||||||
stages[0].status = 'completed';
|
stages[0].status = 'completed';
|
||||||
stages[0].detail = `${imagePaths.length} page(s) ready for AI.`;
|
stages[0].detail = `${imagePaths.length} page(s) ready for AI.`;
|
||||||
await job.updateProgress({ stages });
|
await job.updateProgress({ stages });
|
||||||
@@ -84,7 +86,9 @@ export class FlyerProcessingService {
|
|||||||
stages[1].status = 'in-progress';
|
stages[1].status = 'in-progress';
|
||||||
await job.updateProgress({ stages });
|
await job.updateProgress({ stages });
|
||||||
|
|
||||||
|
console.error(`[WORKER DEBUG] ProcessingService: Calling aiProcessor.extractAndValidateData`);
|
||||||
const aiResult = await this.aiProcessor.extractAndValidateData(imagePaths, job.data, logger);
|
const aiResult = await this.aiProcessor.extractAndValidateData(imagePaths, job.data, logger);
|
||||||
|
console.error(`[WORKER DEBUG] ProcessingService: aiProcessor returned data for store: ${aiResult.data.store_name}`);
|
||||||
stages[1].status = 'completed';
|
stages[1].status = 'completed';
|
||||||
await job.updateProgress({ stages });
|
await job.updateProgress({ stages });
|
||||||
|
|
||||||
@@ -97,13 +101,19 @@ export class FlyerProcessingService {
|
|||||||
const primaryImagePath = imagePaths[0].path;
|
const primaryImagePath = imagePaths[0].path;
|
||||||
const imageFileName = path.basename(primaryImagePath);
|
const imageFileName = path.basename(primaryImagePath);
|
||||||
const iconsDir = path.join(path.dirname(primaryImagePath), 'icons');
|
const iconsDir = path.join(path.dirname(primaryImagePath), 'icons');
|
||||||
|
console.error(`[WORKER DEBUG] ProcessingService: Generating icon from ${primaryImagePath} to ${iconsDir}`);
|
||||||
const iconFileName = await generateFlyerIcon(primaryImagePath, iconsDir, logger);
|
const iconFileName = await generateFlyerIcon(primaryImagePath, iconsDir, logger);
|
||||||
|
console.error(`[WORKER DEBUG] ProcessingService: Icon generated: ${iconFileName}`);
|
||||||
|
|
||||||
// Add the newly generated icon to the list of files to be cleaned up.
|
// Add the newly generated icon to the list of files to be cleaned up.
|
||||||
// The main processed image path is already in `allFilePaths` via `createdImagePaths`.
|
// The main processed image path is already in `allFilePaths` via `createdImagePaths`.
|
||||||
allFilePaths.push(path.join(iconsDir, iconFileName));
|
allFilePaths.push(path.join(iconsDir, iconFileName));
|
||||||
|
|
||||||
console.log('[DEBUG] FlyerProcessingService calling transformer with:', { originalFileName: job.data.originalFileName, imageFileName, iconFileName, checksum: job.data.checksum, baseUrl: job.data.baseUrl });
|
// Ensure we have a valid base URL, preferring the one from the job data.
|
||||||
|
// This is critical for workers where process.env.FRONTEND_URL might be undefined.
|
||||||
|
const baseUrl = job.data.baseUrl || process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||||
|
console.error(`[DEBUG] FlyerProcessingService resolved baseUrl: "${baseUrl}" (job.data.baseUrl: "${job.data.baseUrl}", env.FRONTEND_URL: "${process.env.FRONTEND_URL}")`);
|
||||||
|
console.error('[DEBUG] FlyerProcessingService calling transformer with:', { originalFileName: job.data.originalFileName, imageFileName, iconFileName, checksum: job.data.checksum, baseUrl });
|
||||||
|
|
||||||
const { flyerData, itemsForDb } = await this.transformer.transform(
|
const { flyerData, itemsForDb } = await this.transformer.transform(
|
||||||
aiResult,
|
aiResult,
|
||||||
@@ -113,8 +123,10 @@ export class FlyerProcessingService {
|
|||||||
job.data.checksum,
|
job.data.checksum,
|
||||||
job.data.userId,
|
job.data.userId,
|
||||||
logger,
|
logger,
|
||||||
job.data.baseUrl,
|
baseUrl,
|
||||||
);
|
);
|
||||||
|
console.error('[DEBUG] FlyerProcessingService transformer output URLs:', { imageUrl: flyerData.image_url, iconUrl: flyerData.icon_url });
|
||||||
|
console.error('[DEBUG] Full Flyer Data to be saved:', JSON.stringify(flyerData, null, 2));
|
||||||
stages[2].status = 'completed';
|
stages[2].status = 'completed';
|
||||||
await job.updateProgress({ stages });
|
await job.updateProgress({ stages });
|
||||||
|
|
||||||
@@ -145,6 +157,12 @@ export class FlyerProcessingService {
|
|||||||
});
|
});
|
||||||
flyerId = flyer.flyer_id;
|
flyerId = flyer.flyer_id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Capture specific validation errors and append context for debugging
|
||||||
|
if (error instanceof Error && error.message.includes('Invalid URL')) {
|
||||||
|
const msg = `DB Validation Failed: ${error.message}. ImageURL: '${flyerData.image_url}', IconURL: '${flyerData.icon_url}'`;
|
||||||
|
console.error('[ERROR] ' + msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
if (error instanceof FlyerProcessingError) throw error;
|
if (error instanceof FlyerProcessingError) throw error;
|
||||||
throw new DatabaseError(error instanceof Error ? error.message : String(error));
|
throw new DatabaseError(error instanceof Error ? error.message : String(error));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
|||||||
if (jobStatus?.state === 'failed') {
|
if (jobStatus?.state === 'failed') {
|
||||||
console.error('[DEBUG] Job failed with reason:', jobStatus.failedReason);
|
console.error('[DEBUG] Job failed with reason:', jobStatus.failedReason);
|
||||||
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
||||||
|
console.error('[DEBUG] Job return value:', JSON.stringify(jobStatus.returnValue, null, 2));
|
||||||
console.error('[DEBUG] Full Job Status:', JSON.stringify(jobStatus, null, 2));
|
console.error('[DEBUG] Full Job Status:', JSON.stringify(jobStatus, null, 2));
|
||||||
}
|
}
|
||||||
expect(jobStatus?.state).toBe('completed');
|
expect(jobStatus?.state).toBe('completed');
|
||||||
|
|||||||
@@ -117,15 +117,28 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName));
|
createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName));
|
||||||
|
|
||||||
// --- Act 1: Upload the flyer to trigger the background job ---
|
// --- Act 1: Upload the flyer to trigger the background job ---
|
||||||
|
const testBaseUrl = 'https://example.com';
|
||||||
|
console.error('--------------------------------------------------------------------------------');
|
||||||
|
console.error('[TEST DEBUG] STARTING UPLOAD STEP');
|
||||||
|
console.error(`[TEST DEBUG] Env FRONTEND_URL: "${process.env.FRONTEND_URL}"`);
|
||||||
|
console.error(`[TEST DEBUG] Sending baseUrl field: "${testBaseUrl}"`);
|
||||||
|
console.error('--------------------------------------------------------------------------------');
|
||||||
|
|
||||||
const uploadResponse = await request
|
const uploadResponse = await request
|
||||||
.post('/api/ai/upload-and-process')
|
.post('/api/ai/upload-and-process')
|
||||||
.set('Authorization', `Bearer ${authToken}`)
|
.set('Authorization', `Bearer ${authToken}`)
|
||||||
.field('checksum', checksum)
|
.field('checksum', checksum)
|
||||||
.field('baseUrl', 'https://example.com')
|
.field('baseUrl', testBaseUrl)
|
||||||
.attach('flyerFile', uniqueContent, uniqueFileName);
|
.attach('flyerFile', uniqueContent, uniqueFileName);
|
||||||
|
|
||||||
|
console.error('--------------------------------------------------------------------------------');
|
||||||
|
console.error(`[TEST DEBUG] Upload Response Status: ${uploadResponse.status}`);
|
||||||
|
console.error(`[TEST DEBUG] Upload Response Body: ${JSON.stringify(uploadResponse.body, null, 2)}`);
|
||||||
|
console.error('--------------------------------------------------------------------------------');
|
||||||
|
|
||||||
const { jobId } = uploadResponse.body;
|
const { jobId } = uploadResponse.body;
|
||||||
expect(jobId).toBeTypeOf('string');
|
expect(jobId).toBeTypeOf('string');
|
||||||
|
console.error(`[TEST DEBUG] Job ID received: ${jobId}`);
|
||||||
|
|
||||||
// --- Act 2: Poll for job completion using the new utility ---
|
// --- Act 2: Poll for job completion using the new utility ---
|
||||||
const jobStatus = await poll(
|
const jobStatus = await poll(
|
||||||
@@ -133,6 +146,7 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
const statusResponse = await request
|
const statusResponse = await request
|
||||||
.get(`/api/ai/jobs/${jobId}/status`)
|
.get(`/api/ai/jobs/${jobId}/status`)
|
||||||
.set('Authorization', `Bearer ${authToken}`);
|
.set('Authorization', `Bearer ${authToken}`);
|
||||||
|
console.error(`[TEST DEBUG] Polling status for ${jobId}: ${statusResponse.body?.state}`);
|
||||||
return statusResponse.body;
|
return statusResponse.body;
|
||||||
},
|
},
|
||||||
(status) => status.state === 'completed' || status.state === 'failed',
|
(status) => status.state === 'completed' || status.state === 'failed',
|
||||||
@@ -144,6 +158,17 @@ describe('Gamification Flow Integration Test', () => {
|
|||||||
throw new Error('Gamification test job timed out: No job status received.');
|
throw new Error('Gamification test job timed out: No job status received.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.error('--------------------------------------------------------------------------------');
|
||||||
|
console.error('[TEST DEBUG] Final Job Status Object:', JSON.stringify(jobStatus, null, 2));
|
||||||
|
if (jobStatus.state === 'failed') {
|
||||||
|
console.error(`[TEST DEBUG] Job Failed Reason: ${jobStatus.failedReason}`);
|
||||||
|
// If there is a progress object with error details, log it
|
||||||
|
if (jobStatus.progress) {
|
||||||
|
console.error(`[TEST DEBUG] Job Progress/Error Details:`, JSON.stringify(jobStatus.progress, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error('--------------------------------------------------------------------------------');
|
||||||
|
|
||||||
// --- Assert 1: Verify the job completed successfully ---
|
// --- Assert 1: Verify the job completed successfully ---
|
||||||
if (jobStatus?.state === 'failed') {
|
if (jobStatus?.state === 'failed') {
|
||||||
console.error('[DEBUG] Gamification test job failed:', jobStatus.failedReason);
|
console.error('[DEBUG] Gamification test job failed:', jobStatus.failedReason);
|
||||||
|
|||||||
@@ -245,8 +245,6 @@ describe('Public API Routes Integration Tests', () => {
|
|||||||
|
|
||||||
expect(blockedResponse).toBeDefined();
|
expect(blockedResponse).toBeDefined();
|
||||||
expect(blockedResponse.status).toBe(429);
|
expect(blockedResponse.status).toBe(429);
|
||||||
expect(blockedResponse.headers).toHaveProperty('x-ratelimit-limit');
|
|
||||||
expect(blockedResponse.headers).toHaveProperty('x-ratelimit-remaining');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user