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
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m6s
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user