Files
flyer-crawler.projectium.com/src/routes/upc.routes.ts
Torben Sorensen 2d2cd52011
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
Massive Dependency Modernization Project
2026-02-13 00:34:22 -08:00

245 lines
6.9 KiB
TypeScript

// src/routes/upc.routes.ts
/**
* @file UPC Scanning API Routes
* Provides endpoints for UPC barcode scanning, lookup, and scan history.
*/
import express, { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import passport, { isAdmin } from '../config/passport';
import type { UserProfile } from '../types';
import { validateRequest } from '../middleware/validation.middleware';
import { numericIdParam, optionalNumeric } from '../utils/zodUtils';
import { sendSuccess, sendNoContent } from '../utils/apiResponse';
import * as upcService from '../services/upcService.server';
const router = express.Router();
// --- Zod Schemas for UPC Routes ---
/**
* UPC code validation (8-14 digits)
*/
const upcCodeSchema = z.string().regex(/^[0-9]{8,14}$/, 'UPC code must be 8-14 digits.');
/**
* Scan source validation
*/
const scanSourceSchema = z.enum(['image_upload', 'manual_entry', 'phone_app', 'camera_scan']);
/**
* Schema for UPC scan request
*/
const scanUpcSchema = z.object({
body: z
.object({
upc_code: z.string().optional(),
image_base64: z.string().optional(),
scan_source: scanSourceSchema,
})
.refine((data) => data.upc_code || data.image_base64, {
message: 'Either upc_code or image_base64 must be provided.',
}),
});
/**
* Schema for UPC lookup request (without recording scan)
*/
const lookupUpcSchema = z.object({
query: z.object({
upc_code: upcCodeSchema,
include_external: z
.string()
.optional()
.transform((val) => val === 'true'),
force_refresh: z
.string()
.optional()
.transform((val) => val === 'true'),
}),
});
/**
* Schema for linking UPC to product (admin)
*/
const linkUpcSchema = z.object({
body: z.object({
upc_code: upcCodeSchema,
product_id: z.number().int().positive('Product ID must be a positive integer.'),
}),
});
/**
* Schema for scan ID parameter
*/
const scanIdParamSchema = numericIdParam(
'scanId',
"Invalid ID for parameter 'scanId'. Must be a number.",
);
/**
* Schema for scan history query
*/
const scanHistoryQuerySchema = z.object({
query: z.object({
limit: optionalNumeric({ default: 50, min: 1, max: 100, integer: true }),
offset: optionalNumeric({ default: 0, min: 0, integer: true }),
lookup_successful: z
.string()
.optional()
.transform((val) => (val === 'true' ? true : val === 'false' ? false : undefined)),
scan_source: scanSourceSchema.optional(),
from_date: z.string().date().optional(),
to_date: z.string().date().optional(),
}),
});
// Middleware to ensure user is authenticated for all UPC routes
router.use(passport.authenticate('jwt', { session: false }));
router.post(
'/scan',
validateRequest(scanUpcSchema),
async (req: Request, res: Response, next: NextFunction) => {
const userProfile = req.user as UserProfile;
type ScanUpcRequest = z.infer<typeof scanUpcSchema>;
const { body } = req as unknown as ScanUpcRequest;
try {
req.log.info(
{ userId: userProfile.user.user_id, scanSource: body.scan_source },
'UPC scan request received',
);
const result = await upcService.scanUpc(userProfile.user.user_id, body, req.log);
sendSuccess(res, result);
} catch (error) {
req.log.error(
{ error, userId: userProfile.user.user_id, scanSource: body.scan_source },
'Error processing UPC scan',
);
next(error);
}
},
);
router.get(
'/lookup',
validateRequest(lookupUpcSchema),
async (req: Request, res: Response, next: NextFunction) => {
type LookupUpcRequest = z.infer<typeof lookupUpcSchema>;
const { query } = req as unknown as LookupUpcRequest;
try {
req.log.debug({ upcCode: query.upc_code }, 'UPC lookup request received');
const result = await upcService.lookupUpc(
{
upc_code: query.upc_code,
force_refresh: query.force_refresh,
},
req.log,
);
sendSuccess(res, result);
} catch (error) {
req.log.error({ error, upcCode: query.upc_code }, 'Error looking up UPC');
next(error);
}
},
);
router.get(
'/history',
validateRequest(scanHistoryQuerySchema),
async (req: Request, res: Response, next: NextFunction) => {
const userProfile = req.user as UserProfile;
type ScanHistoryRequest = z.infer<typeof scanHistoryQuerySchema>;
const { query } = req as unknown as ScanHistoryRequest;
try {
const result = await upcService.getScanHistory(
{
user_id: userProfile.user.user_id,
limit: query.limit,
offset: query.offset,
lookup_successful: query.lookup_successful,
scan_source: query.scan_source,
from_date: query.from_date,
to_date: query.to_date,
},
req.log,
);
sendSuccess(res, result);
} catch (error) {
req.log.error({ error, userId: userProfile.user.user_id }, 'Error fetching scan history');
next(error);
}
},
);
router.get(
'/history/:scanId',
validateRequest(scanIdParamSchema),
async (req: Request, res: Response, next: NextFunction) => {
const userProfile = req.user as UserProfile;
type GetScanRequest = z.infer<typeof scanIdParamSchema>;
const { params } = req as unknown as GetScanRequest;
try {
const scan = await upcService.getScanById(params.scanId, userProfile.user.user_id, req.log);
sendSuccess(res, scan);
} catch (error) {
req.log.error(
{ error, userId: userProfile.user.user_id, scanId: params.scanId },
'Error fetching scan by ID',
);
next(error);
}
},
);
router.get('/stats', async (req: Request, res: Response, next: NextFunction) => {
const userProfile = req.user as UserProfile;
try {
const stats = await upcService.getScanStats(userProfile.user.user_id, req.log);
sendSuccess(res, stats);
} catch (error) {
req.log.error({ error, userId: userProfile.user.user_id }, 'Error fetching scan statistics');
next(error);
}
});
router.post(
'/link',
isAdmin, // Admin role check - only admins can link UPC codes to products
validateRequest(linkUpcSchema),
async (req: Request, res: Response, next: NextFunction) => {
const userProfile = req.user as UserProfile;
type LinkUpcRequest = z.infer<typeof linkUpcSchema>;
const { body } = req as unknown as LinkUpcRequest;
try {
req.log.info(
{ userId: userProfile.user.user_id, productId: body.product_id, upcCode: body.upc_code },
'UPC link request received',
);
await upcService.linkUpcToProduct(body.product_id, body.upc_code, req.log);
sendNoContent(res);
} catch (error) {
req.log.error(
{
error,
userId: userProfile.user.user_id,
productId: body.product_id,
upcCode: body.upc_code,
},
'Error linking UPC to product',
);
next(error);
}
},
);
export default router;