All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 1m1s
209 lines
8.8 KiB
TypeScript
209 lines
8.8 KiB
TypeScript
// src/routes/ai.ts
|
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
import multer from 'multer';
|
|
import passport from './passport';
|
|
import { optionalAuth } from './passport';
|
|
import * as db from '../services/db';
|
|
import * as aiService from '../services/aiService.server'; // Correctly import server-side AI service
|
|
import { logger } from '../services/logger.server';
|
|
import { UserProfile } from '../types';
|
|
|
|
const router = Router();
|
|
|
|
// --- Multer Configuration for File Uploads ---
|
|
const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/assets';
|
|
const storage = multer.diskStorage({
|
|
destination: function (req, file, cb) {
|
|
cb(null, storagePath);
|
|
},
|
|
filename: function (req, file, cb) {
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
cb(null, file.fieldname + '-' + uniqueSuffix + '-' + file.originalname);
|
|
}
|
|
});
|
|
const upload = multer({ storage: storage });
|
|
|
|
/**
|
|
* This endpoint processes a flyer using AI. It uses `optionalAuth` middleware to allow
|
|
* both authenticated and anonymous users to upload flyers.
|
|
*/
|
|
router.post('/process-flyer', optionalAuth, upload.array('flyerImages'), async (req: Request, res: Response, next: NextFunction) => {
|
|
// --- AI ROUTE DEBUG LOGGING ---
|
|
logger.debug('[API /ai/process-flyer] Request received.');
|
|
logger.debug(`[API /ai/process-flyer] Files received: ${req.files ? (req.files as Express.Multer.File[]).length : 0}`);
|
|
logger.debug(`[API /ai/process-flyer] Body masterItems (first 50 chars): ${req.body.masterItems?.substring(0, 50)}...`);
|
|
// --- END DEBUG LOGGING ---
|
|
try {
|
|
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
|
|
return res.status(400).json({ message: 'Flyer image files are required.' });
|
|
}
|
|
|
|
const masterItems = JSON.parse(req.body.masterItems);
|
|
const imagePaths = (req.files as Express.Multer.File[]).map(file => ({
|
|
path: file.path,
|
|
mimetype: file.mimetype
|
|
}));
|
|
|
|
logger.debug(`[API /ai/process-flyer] Processing image paths:`, { imagePaths });
|
|
|
|
const user = req.user as UserProfile | undefined;
|
|
const logIdentifier = user ? `user ID: ${user.user_id}` : 'anonymous user';
|
|
|
|
logger.info(`Starting AI flyer data extraction for ${imagePaths.length} image(s) for ${logIdentifier}.`);
|
|
|
|
const extractedData = await aiService.extractCoreDataFromFlyerImage(imagePaths, masterItems);
|
|
logger.info(`Completed AI flyer data extraction. Found ${extractedData.items.length} items.`);
|
|
|
|
res.status(200).json({ data: extractedData });
|
|
} catch (error) {
|
|
logger.error('Error in /api/ai/process-flyer endpoint:', { error });
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This endpoint saves the processed flyer data to the database. It is the final step
|
|
* in the flyer upload workflow after the AI has extracted the data.
|
|
* It uses `optionalAuth` to handle submissions from both anonymous and authenticated users.
|
|
*/
|
|
router.post('/flyers/process', optionalAuth, upload.single('flyerImage'), async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
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.' });
|
|
}
|
|
|
|
const { checksum, originalFileName, extractedData } = JSON.parse(req.body.data);
|
|
const user = req.user as UserProfile | undefined;
|
|
|
|
// 1. Check for duplicate flyer using checksum
|
|
const existingFlyer = await db.findFlyerByChecksum(checksum);
|
|
if (existingFlyer) {
|
|
logger.warn(`Duplicate flyer upload attempt blocked for checksum: ${checksum}`);
|
|
return res.status(409).json({ message: 'This flyer has already been processed.' });
|
|
}
|
|
|
|
// Find or create the store to get its ID, which is required by `createFlyerAndItems`.
|
|
let storeId: number;
|
|
const storeRes = await db.getPool().query<{ store_id: number }>('SELECT store_id FROM public.stores WHERE name = $1', [extractedData.store_name]);
|
|
if (storeRes.rows.length > 0) {
|
|
storeId = storeRes.rows[0].store_id;
|
|
} else {
|
|
const newStoreRes = await db.getPool().query<{ store_id: number }>('INSERT INTO public.stores (name) VALUES ($1) RETURNING store_id', [extractedData.store_name]);
|
|
storeId = newStoreRes.rows[0].store_id;
|
|
}
|
|
|
|
// 2. Prepare flyer data for insertion
|
|
const flyerData = {
|
|
file_name: originalFileName,
|
|
image_url: `/assets/${req.file.filename}`,
|
|
checksum: checksum,
|
|
store_name: extractedData.store_name,
|
|
store_id: storeId, // Use the resolved store_id
|
|
valid_from: extractedData.valid_from,
|
|
valid_to: extractedData.valid_to,
|
|
store_address: extractedData.store_address,
|
|
uploaded_by: user?.user_id, // Associate with user if logged in
|
|
};
|
|
|
|
// 3. Create flyer and its items in a transaction
|
|
const newFlyer = await db.createFlyerAndItems(flyerData, extractedData.items);
|
|
|
|
logger.info(`Successfully processed and saved new flyer: ${newFlyer.file_name} (ID: ${newFlyer.flyer_id})`);
|
|
|
|
// Log this significant event
|
|
await db.logActivity({
|
|
userId: user?.user_id,
|
|
action: 'flyer_processed',
|
|
displayText: `Processed a new flyer for ${flyerData.store_name}.`,
|
|
details: { flyerId: newFlyer.flyer_id, storeName: flyerData.store_name }
|
|
});
|
|
|
|
res.status(201).json({ message: 'Flyer processed and saved successfully.', flyer: newFlyer });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This endpoint checks if an image is a flyer. It uses `optionalAuth` to allow
|
|
* both authenticated and anonymous users to perform this check.
|
|
*/
|
|
router.post('/check-flyer', optionalAuth, upload.single('image'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ message: 'Image file is required.' });
|
|
}
|
|
logger.info(`Server-side flyer check for file: ${req.file.originalname}`);
|
|
res.status(200).json({ is_flyer: true }); // Stubbed response
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/extract-address', optionalAuth, upload.single('image'), async (req, res, next) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ message: 'Image file is required.' });
|
|
}
|
|
logger.info(`Server-side address extraction for file: ${req.file.originalname}`);
|
|
res.status(200).json({ address: "123 AI Street, Server City" }); // Stubbed response
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/extract-logo', optionalAuth, upload.array('images'), async (req, res, next) => {
|
|
try {
|
|
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
|
|
return res.status(400).json({ message: 'Image files are required.' });
|
|
}
|
|
logger.info(`Server-side logo extraction for ${req.files.length} image(s).`);
|
|
res.status(200).json({ store_logo_base_64: null }); // Stubbed response
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/quick-insights', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
|
|
try {
|
|
logger.info(`Server-side quick insights requested.`);
|
|
res.status(200).json({ text: "This is a server-generated quick insight: buy the cheap stuff!" }); // Stubbed response
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/deep-dive', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
|
|
try {
|
|
logger.info(`Server-side deep dive requested.`);
|
|
res.status(200).json({ text: "This is a server-generated deep dive analysis. It is very detailed." }); // Stubbed response
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/search-web', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
|
|
try {
|
|
logger.info(`Server-side web search requested.`);
|
|
res.status(200).json({ text: "The web says this is good.", sources: [] }); // Stubbed response
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
router.post('/plan-trip', passport.authenticate('jwt', { session: false }), async (req, res, next) => {
|
|
try {
|
|
const { items, store, userLocation } = req.body;
|
|
logger.info(`Server-side trip planning requested for user.`);
|
|
const result = await aiService.planTripWithMaps(items, store, userLocation);
|
|
res.status(200).json(result);
|
|
} catch (error) {
|
|
logger.error('Error in /api/ai/plan-trip endpoint:', { error });
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default router; |