All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m54s
392 lines
11 KiB
TypeScript
392 lines
11 KiB
TypeScript
// src/routes/flyer.routes.ts
|
|
import { Router } from 'express';
|
|
import * as db from '../services/db/index.db';
|
|
import { z } from 'zod';
|
|
import { validateRequest } from '../middleware/validation.middleware';
|
|
import { optionalNumeric } from '../utils/zodUtils';
|
|
import { publicReadLimiter, batchLimiter, trackingLimiter } from '../config/rateLimiters';
|
|
import { sendSuccess } from '../utils/apiResponse';
|
|
|
|
const router = Router();
|
|
|
|
// --- Zod Schemas for Flyer Routes ---
|
|
|
|
const getFlyersSchema = z.object({
|
|
query: z.object({
|
|
limit: optionalNumeric({ default: 20, integer: true, positive: true }),
|
|
offset: optionalNumeric({ default: 0, integer: true, nonnegative: true }),
|
|
}),
|
|
});
|
|
|
|
const flyerIdParamSchema = z.object({
|
|
params: z.object({
|
|
id: z.coerce.number().int('Invalid flyer ID provided.').positive('Invalid flyer ID provided.'),
|
|
}),
|
|
});
|
|
|
|
const batchFetchSchema = z.object({
|
|
body: z.object({
|
|
flyerIds: z.array(z.number().int().positive()).min(1, 'flyerIds must be a non-empty array.'),
|
|
}),
|
|
});
|
|
|
|
const batchCountSchema = z.object({
|
|
body: z.object({
|
|
flyerIds: z.array(z.number().int().positive()),
|
|
}),
|
|
});
|
|
|
|
const trackItemSchema = z.object({
|
|
params: z.object({
|
|
itemId: z.coerce.number().int().positive('Invalid item ID provided.'),
|
|
}),
|
|
body: z.object({
|
|
type: z.enum(['view', 'click'], {
|
|
message: 'A valid interaction type ("view" or "click") is required.',
|
|
}),
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /flyers:
|
|
* get:
|
|
* summary: Get all flyers
|
|
* description: Returns a paginated list of all flyers.
|
|
* tags:
|
|
* - Flyers
|
|
* parameters:
|
|
* - in: query
|
|
* name: limit
|
|
* schema:
|
|
* type: integer
|
|
* default: 20
|
|
* description: Maximum number of flyers to return
|
|
* - in: query
|
|
* name: offset
|
|
* schema:
|
|
* type: integer
|
|
* default: 0
|
|
* description: Number of flyers to skip
|
|
* responses:
|
|
* 200:
|
|
* description: List of flyers
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* flyer_id:
|
|
* type: integer
|
|
* store_id:
|
|
* type: integer
|
|
* flyer_name:
|
|
* type: string
|
|
* start_date:
|
|
* type: string
|
|
* format: date
|
|
* end_date:
|
|
* type: string
|
|
* format: date
|
|
*/
|
|
router.get(
|
|
'/',
|
|
publicReadLimiter,
|
|
validateRequest(getFlyersSchema),
|
|
async (req, res, next): Promise<void> => {
|
|
try {
|
|
// The `validateRequest` middleware ensures `req.query` is valid.
|
|
// We parse it here to apply Zod's coercions (string to number) and defaults.
|
|
const { limit, offset } = getFlyersSchema.shape.query.parse(req.query);
|
|
|
|
const flyers = await db.flyerRepo.getFlyers(req.log, limit, offset);
|
|
sendSuccess(res, flyers);
|
|
} catch (error) {
|
|
req.log.error({ error }, 'Error fetching flyers in /api/flyers:');
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /flyers/{id}:
|
|
* get:
|
|
* summary: Get flyer by ID
|
|
* description: Returns a single flyer by its ID.
|
|
* tags:
|
|
* - Flyers
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The flyer ID
|
|
* responses:
|
|
* 200:
|
|
* description: Flyer details
|
|
* 404:
|
|
* description: Flyer not found
|
|
*/
|
|
router.get(
|
|
'/:id',
|
|
publicReadLimiter,
|
|
validateRequest(flyerIdParamSchema),
|
|
async (req, res, next): Promise<void> => {
|
|
try {
|
|
// Explicitly parse to get the coerced number type for `id`.
|
|
const { id } = flyerIdParamSchema.shape.params.parse(req.params);
|
|
const flyer = await db.flyerRepo.getFlyerById(id);
|
|
sendSuccess(res, flyer);
|
|
} catch (error) {
|
|
req.log.error({ error, flyerId: req.params.id }, 'Error fetching flyer by ID:');
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /flyers/{id}/items:
|
|
* get:
|
|
* summary: Get flyer items
|
|
* description: Returns all items (deals) for a specific flyer.
|
|
* tags:
|
|
* - Flyers
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The flyer ID
|
|
* responses:
|
|
* 200:
|
|
* description: List of flyer items
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* item_id:
|
|
* type: integer
|
|
* item_name:
|
|
* type: string
|
|
* price:
|
|
* type: number
|
|
* unit:
|
|
* type: string
|
|
*/
|
|
router.get(
|
|
'/:id/items',
|
|
publicReadLimiter,
|
|
validateRequest(flyerIdParamSchema),
|
|
async (req, res, next): Promise<void> => {
|
|
try {
|
|
// Explicitly parse to get the coerced number type for `id`.
|
|
const { id } = flyerIdParamSchema.shape.params.parse(req.params);
|
|
const items = await db.flyerRepo.getFlyerItems(id, req.log);
|
|
sendSuccess(res, items);
|
|
} catch (error) {
|
|
req.log.error(
|
|
{ error, flyerId: req.params.id },
|
|
'Error fetching flyer items in /api/flyers/:id/items:',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /flyers/items/batch-fetch:
|
|
* post:
|
|
* summary: Batch fetch flyer items
|
|
* description: Returns all items for multiple flyers in a single request.
|
|
* tags:
|
|
* - Flyers
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - flyerIds
|
|
* properties:
|
|
* flyerIds:
|
|
* type: array
|
|
* items:
|
|
* type: integer
|
|
* minItems: 1
|
|
* example: [1, 2, 3]
|
|
* responses:
|
|
* 200:
|
|
* description: Items for all requested flyers
|
|
*/
|
|
type BatchFetchRequest = z.infer<typeof batchFetchSchema>;
|
|
router.post(
|
|
'/items/batch-fetch',
|
|
batchLimiter,
|
|
validateRequest(batchFetchSchema),
|
|
async (req, res, next): Promise<void> => {
|
|
const { body } = req as unknown as BatchFetchRequest;
|
|
try {
|
|
// No re-parsing needed here as `validateRequest` has already ensured the body shape,
|
|
// and `express.json()` has parsed it. There's no type coercion to apply.
|
|
const items = await db.flyerRepo.getFlyerItemsForFlyers(body.flyerIds, req.log);
|
|
sendSuccess(res, items);
|
|
} catch (error) {
|
|
req.log.error({ error }, 'Error fetching batch flyer items');
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /flyers/items/batch-count:
|
|
* post:
|
|
* summary: Batch count flyer items
|
|
* description: Returns the total item count for multiple flyers.
|
|
* tags:
|
|
* - Flyers
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - flyerIds
|
|
* properties:
|
|
* flyerIds:
|
|
* type: array
|
|
* items:
|
|
* type: integer
|
|
* example: [1, 2, 3]
|
|
* responses:
|
|
* 200:
|
|
* description: Total item count
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* count:
|
|
* type: integer
|
|
* example: 42
|
|
*/
|
|
type BatchCountRequest = z.infer<typeof batchCountSchema>;
|
|
router.post(
|
|
'/items/batch-count',
|
|
batchLimiter,
|
|
validateRequest(batchCountSchema),
|
|
async (req, res, next): Promise<void> => {
|
|
const { body } = req as unknown as BatchCountRequest;
|
|
try {
|
|
// The schema ensures flyerIds is an array of numbers.
|
|
// The `?? []` was redundant as `validateRequest` would have already caught a missing `flyerIds`.
|
|
const count = await db.flyerRepo.countFlyerItemsForFlyers(body.flyerIds, req.log);
|
|
sendSuccess(res, { count });
|
|
} catch (error) {
|
|
req.log.error({ error }, 'Error counting batch flyer items');
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /flyers/items/{itemId}/track:
|
|
* post:
|
|
* summary: Track item interaction
|
|
* description: Records a view or click interaction with a flyer item for analytics.
|
|
* tags:
|
|
* - Flyers
|
|
* parameters:
|
|
* - in: path
|
|
* name: itemId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The flyer item ID
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - type
|
|
* properties:
|
|
* type:
|
|
* type: string
|
|
* enum: [view, click]
|
|
* description: Type of interaction
|
|
* responses:
|
|
* 202:
|
|
* description: Tracking accepted (fire-and-forget)
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* example: true
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Tracking accepted
|
|
*/
|
|
router.post(
|
|
'/items/:itemId/track',
|
|
trackingLimiter,
|
|
validateRequest(trackItemSchema),
|
|
(req, res, next): void => {
|
|
try {
|
|
// Explicitly parse to get coerced types.
|
|
const { params, body } = trackItemSchema.parse({ params: req.params, body: req.body });
|
|
|
|
// Fire-and-forget: we don't await the tracking call to avoid delaying the response.
|
|
// We add a .catch to log any potential errors without crashing the server process.
|
|
db.flyerRepo.trackFlyerItemInteraction(params.itemId, body.type, req.log).catch((error) => {
|
|
req.log.error({ error, itemId: params.itemId }, 'Flyer item interaction tracking failed');
|
|
});
|
|
|
|
sendSuccess(res, { message: 'Tracking accepted' }, 202);
|
|
} catch (error) {
|
|
// This will catch Zod parsing errors if they occur.
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
export default router;
|