Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
245 lines
6.9 KiB
TypeScript
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;
|