ai: tolerant parsing + diagnostics for /api/ai/flyers/process; fix test assertion
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m6s

This commit is contained in:
2025-12-02 11:52:26 -08:00
parent 79d095d6b9
commit a8f650d513
2 changed files with 68 additions and 6 deletions

View File

@@ -192,7 +192,8 @@ describe('AI Routes (/api/ai)', () => {
const callArgs = mockedDb.createFlyerAndItems.mock.calls[0]?.[1];
expect(callArgs).toBeDefined();
expect(Array.isArray(callArgs)).toBe(true);
expect(callArgs.length).toBe(0);
// use non-null assertion for the runtime-checked variable so TypeScript is satisfied
expect(callArgs!.length).toBe(0);
});
it('should fallback to a safe store name when store_name is missing', async () => {

View File

@@ -13,6 +13,14 @@ import { UserProfile } from '../types';
const router = Router();
// Helper to safely extract an error message from unknown `catch` values.
const errMsg = (e: unknown) => {
if (!e) return String(e);
if (typeof e === 'string') return e;
if (typeof e === 'object' && 'message' in e) return String((e as any).message);
return String(e);
};
// --- Multer Configuration for File Uploads ---
const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/assets';
@@ -102,18 +110,71 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
if (!req.file) {
return res.status(400).json({ message: 'Flyer image file is required.' });
}
if (!req.body.data) {
return res.status(400).json({ message: 'Data payload is required.' });
// Diagnostic & tolerant parsing for flyers/process
logger.debug('[API /ai/flyers/process] req.body keys:', Object.keys(req.body || {}));
logger.debug('[API /ai/flyers/process] file present:', !!req.file);
// Try several ways to obtain the payload so we are tolerant to client variations.
let parsed: any = {};
let extractedData: any = {};
try {
// If the client sent a top-level `data` field (stringified JSON), parse it.
if (req.body && (req.body.data || req.body.extractedData)) {
const raw = (req.body.data ?? req.body.extractedData) as any;
logger.debug('[API /ai/flyers/process] raw extractedData type:', typeof raw, 'length:', raw && raw.length ? raw.length : 0);
try {
parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (err) {
logger.warn('[API /ai/flyers/process] Failed to JSON.parse raw extractedData; falling back to direct assign', { error: errMsg(err) });
parsed = typeof raw === 'string' ? JSON.parse(String(raw).slice(0, 2000)) : raw;
}
// If parsed itself contains an `extractedData` field, use that, otherwise assume parsed is the extractedData
extractedData = parsed.extractedData ?? parsed;
} else {
// No explicit `data` field found. Attempt to interpret req.body as an object (Express may have parsed multipart fields differently).
try {
parsed = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
} catch (err) {
logger.warn('[API /ai/flyers/process] Failed to JSON.parse req.body; using empty object', { error: errMsg(err) });
parsed = req.body || {};
}
// extractedData might be nested under `data` or `extractedData`, or the body itself may be the extracted data.
if (parsed.data) {
try {
const inner = typeof parsed.data === 'string' ? JSON.parse(parsed.data) : parsed.data;
extractedData = inner.extractedData ?? inner;
} catch (err) {
logger.warn('[API /ai/flyers/process] Failed to parse parsed.data; falling back', { error: errMsg(err) });
extractedData = parsed.data as any;
}
} else if (parsed.extractedData) {
extractedData = parsed.extractedData;
} else {
// Assume the body itself is the extracted data if it looks like it (has items or store_name keys)
if (parsed.items || parsed.store_name || parsed.valid_from) {
extractedData = parsed;
} else {
extractedData = {};
}
}
}
} catch (err) {
logger.error('[API /ai/flyers/process] Unexpected error while parsing request body', { error: err });
parsed = {};
extractedData = {};
}
const parsed = JSON.parse(req.body.data || '{}');
const { checksum, originalFileName, extractedData } = parsed;
// Pull common metadata fields (checksum, originalFileName) from whichever shape we parsed.
const checksum = parsed.checksum ?? parsed?.data?.checksum ?? undefined;
const originalFileName = parsed.originalFileName ?? parsed?.data?.originalFileName ?? req.file.originalname;
const user = req.user as UserProfile | undefined;
// Validate extractedData to avoid database errors (e.g., null store_name)
if (!extractedData || typeof extractedData !== 'object') {
logger.warn('Missing extractedData in /api/ai/flyers/process payload.', { bodyData: parsed });
return res.status(400).json({ message: 'Invalid request: extractedData is required.' });
// Don't fail hard here; proceed with empty items and fallback store name so the upload can be saved for manual review.
extractedData = {};
}
// Ensure items is an array (DB function handles zero-items case)