Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
283 lines
9.7 KiB
TypeScript
283 lines
9.7 KiB
TypeScript
// src/controllers/flyer.controller.ts
|
|
// ============================================================================
|
|
// FLYER CONTROLLER
|
|
// ============================================================================
|
|
// Provides endpoints for managing flyers and flyer items.
|
|
// Implements endpoints for:
|
|
// - Listing flyers with pagination
|
|
// - Getting individual flyer details
|
|
// - Getting items for a flyer
|
|
// - Batch fetching items for multiple flyers
|
|
// - Batch counting items for multiple flyers
|
|
// - Tracking item interactions (fire-and-forget)
|
|
// ============================================================================
|
|
|
|
import {
|
|
Get,
|
|
Post,
|
|
Route,
|
|
Tags,
|
|
Path,
|
|
Query,
|
|
Body,
|
|
Request,
|
|
SuccessResponse,
|
|
Response,
|
|
} from 'tsoa';
|
|
import type { Request as ExpressRequest } from 'express';
|
|
import { BaseController } from './base.controller';
|
|
import type { SuccessResponse as SuccessResponseType, ErrorResponse } from './types';
|
|
import * as db from '../services/db/index.db';
|
|
import type { FlyerDto, FlyerItemDto } from '../dtos/common.dto';
|
|
|
|
// ============================================================================
|
|
// REQUEST/RESPONSE TYPES
|
|
// ============================================================================
|
|
// Types for request bodies and custom response shapes that will appear in
|
|
// the OpenAPI specification.
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Request body for batch fetching flyer items.
|
|
*/
|
|
interface BatchFetchRequest {
|
|
/**
|
|
* Array of flyer IDs to fetch items for.
|
|
* @minItems 1
|
|
* @example [1, 2, 3]
|
|
*/
|
|
flyerIds: number[];
|
|
}
|
|
|
|
/**
|
|
* Request body for batch counting flyer items.
|
|
*/
|
|
interface BatchCountRequest {
|
|
/**
|
|
* Array of flyer IDs to count items for.
|
|
* @example [1, 2, 3]
|
|
*/
|
|
flyerIds: number[];
|
|
}
|
|
|
|
/**
|
|
* Request body for tracking item interactions.
|
|
*/
|
|
interface TrackInteractionRequest {
|
|
/**
|
|
* Type of interaction to track.
|
|
* @example "view"
|
|
*/
|
|
type: 'view' | 'click';
|
|
}
|
|
|
|
/**
|
|
* Response for batch item count.
|
|
*/
|
|
interface BatchCountResponse {
|
|
/**
|
|
* Total number of items across all requested flyers.
|
|
*/
|
|
count: number;
|
|
}
|
|
|
|
/**
|
|
* Response for tracking confirmation.
|
|
*/
|
|
interface TrackingResponse {
|
|
/**
|
|
* Confirmation message.
|
|
*/
|
|
message: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// FLYER CONTROLLER
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Controller for flyer management endpoints.
|
|
*
|
|
* Provides read-only access to flyers and flyer items for all users,
|
|
* with analytics tracking capabilities.
|
|
*/
|
|
@Route('flyers')
|
|
@Tags('Flyers')
|
|
export class FlyerController extends BaseController {
|
|
// ==========================================================================
|
|
// LIST ENDPOINTS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get all flyers.
|
|
*
|
|
* Returns a paginated list of all flyers, ordered by creation date (newest first).
|
|
* Includes store information and location data for each flyer.
|
|
*
|
|
* @summary List all flyers
|
|
* @param limit Maximum number of flyers to return (default: 20)
|
|
* @param offset Number of flyers to skip for pagination (default: 0)
|
|
* @returns Array of flyer objects with store information
|
|
*/
|
|
@Get()
|
|
@SuccessResponse(200, 'List of flyers retrieved successfully')
|
|
public async getFlyers(
|
|
@Request() req: ExpressRequest,
|
|
@Query() limit?: number,
|
|
@Query() offset?: number,
|
|
): Promise<SuccessResponseType<FlyerDto[]>> {
|
|
// Apply defaults and bounds for pagination
|
|
// Note: Using offset-based pagination to match existing API behavior
|
|
const normalizedLimit = Math.min(100, Math.max(1, Math.floor(limit ?? 20)));
|
|
const normalizedOffset = Math.max(0, Math.floor(offset ?? 0));
|
|
|
|
const flyers = await db.flyerRepo.getFlyers(req.log, normalizedLimit, normalizedOffset);
|
|
// The Flyer type from the repository is structurally compatible with FlyerDto
|
|
// (FlyerDto just omits the GeoJSONPoint type that tsoa can't handle)
|
|
return this.success(flyers as unknown as FlyerDto[]);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// SINGLE RESOURCE ENDPOINTS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get flyer by ID.
|
|
*
|
|
* Returns a single flyer with its full details, including store information
|
|
* and all associated store locations.
|
|
*
|
|
* @summary Get a single flyer
|
|
* @param id The unique identifier of the flyer
|
|
* @returns The flyer object with full details
|
|
*/
|
|
@Get('{id}')
|
|
@SuccessResponse(200, 'Flyer retrieved successfully')
|
|
@Response<ErrorResponse>(404, 'Flyer not found')
|
|
public async getFlyerById(
|
|
@Path() id: number,
|
|
@Request() req: ExpressRequest,
|
|
): Promise<SuccessResponseType<FlyerDto>> {
|
|
// getFlyerById throws NotFoundError if flyer doesn't exist
|
|
// The global error handler converts this to a 404 response
|
|
const flyer = await db.flyerRepo.getFlyerById(id);
|
|
req.log.debug({ flyerId: id }, 'Retrieved flyer by ID');
|
|
return this.success(flyer as unknown as FlyerDto);
|
|
}
|
|
|
|
/**
|
|
* Get flyer items.
|
|
*
|
|
* Returns all items (deals) associated with a specific flyer.
|
|
* Items are ordered by their position in the flyer.
|
|
*
|
|
* @summary Get items for a flyer
|
|
* @param id The unique identifier of the flyer
|
|
* @returns Array of flyer items with pricing and category information
|
|
*/
|
|
@Get('{id}/items')
|
|
@SuccessResponse(200, 'Flyer items retrieved successfully')
|
|
@Response<ErrorResponse>(404, 'Flyer not found')
|
|
public async getFlyerItems(
|
|
@Path() id: number,
|
|
@Request() req: ExpressRequest,
|
|
): Promise<SuccessResponseType<FlyerItemDto[]>> {
|
|
const items = await db.flyerRepo.getFlyerItems(id, req.log);
|
|
req.log.debug({ flyerId: id, itemCount: items.length }, 'Retrieved flyer items');
|
|
return this.success(items as unknown as FlyerItemDto[]);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// BATCH ENDPOINTS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Batch fetch flyer items.
|
|
*
|
|
* Returns all items for multiple flyers in a single request.
|
|
* This is more efficient than making separate requests for each flyer.
|
|
* Items are ordered by flyer ID, then by item position within each flyer.
|
|
*
|
|
* @summary Batch fetch items for multiple flyers
|
|
* @param body Request body containing array of flyer IDs
|
|
* @returns Array of all flyer items for the requested flyers
|
|
*/
|
|
@Post('items/batch-fetch')
|
|
@SuccessResponse(200, 'Batch items retrieved successfully')
|
|
public async batchFetchItems(
|
|
@Body() body: BatchFetchRequest,
|
|
@Request() req: ExpressRequest,
|
|
): Promise<SuccessResponseType<FlyerItemDto[]>> {
|
|
const items = await db.flyerRepo.getFlyerItemsForFlyers(body.flyerIds, req.log);
|
|
req.log.debug(
|
|
{ flyerCount: body.flyerIds.length, itemCount: items.length },
|
|
'Batch fetched flyer items',
|
|
);
|
|
return this.success(items as unknown as FlyerItemDto[]);
|
|
}
|
|
|
|
/**
|
|
* Batch count flyer items.
|
|
*
|
|
* Returns the total item count for multiple flyers.
|
|
* Useful for displaying item counts without fetching all item data.
|
|
*
|
|
* @summary Batch count items for multiple flyers
|
|
* @param body Request body containing array of flyer IDs
|
|
* @returns Object with total count of items across all requested flyers
|
|
*/
|
|
@Post('items/batch-count')
|
|
@SuccessResponse(200, 'Batch count retrieved successfully')
|
|
public async batchCountItems(
|
|
@Body() body: BatchCountRequest,
|
|
@Request() req: ExpressRequest,
|
|
): Promise<SuccessResponseType<BatchCountResponse>> {
|
|
const count = await db.flyerRepo.countFlyerItemsForFlyers(body.flyerIds, req.log);
|
|
req.log.debug({ flyerCount: body.flyerIds.length, totalItems: count }, 'Batch counted items');
|
|
return this.success({ count });
|
|
}
|
|
|
|
// ==========================================================================
|
|
// TRACKING ENDPOINTS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Track item interaction.
|
|
*
|
|
* Records a view or click interaction with a flyer item for analytics purposes.
|
|
* This endpoint uses a fire-and-forget pattern: it returns immediately with a
|
|
* 202 Accepted response while the tracking is processed asynchronously.
|
|
*
|
|
* This design ensures that tracking does not slow down the user experience,
|
|
* and any tracking failures are logged but do not affect the client.
|
|
*
|
|
* @summary Track a flyer item interaction
|
|
* @param itemId The unique identifier of the flyer item
|
|
* @param body The interaction type (view or click)
|
|
* @returns Confirmation that tracking was accepted
|
|
*/
|
|
@Post('items/{itemId}/track')
|
|
@SuccessResponse(202, 'Tracking accepted')
|
|
public async trackItemInteraction(
|
|
@Path() itemId: number,
|
|
@Body() body: TrackInteractionRequest,
|
|
@Request() req: ExpressRequest,
|
|
): Promise<SuccessResponseType<TrackingResponse>> {
|
|
// Fire-and-forget: start the tracking operation but don't await it.
|
|
// We explicitly handle errors in the .catch() to prevent unhandled rejections
|
|
// and to log any failures without affecting the client response.
|
|
db.flyerRepo.trackFlyerItemInteraction(itemId, body.type, req.log).catch((error) => {
|
|
// Log the error but don't propagate it - this is intentional
|
|
// as tracking failures should not impact user experience
|
|
req.log.error(
|
|
{ error, itemId, interactionType: body.type },
|
|
'Flyer item interaction tracking failed (fire-and-forget)',
|
|
);
|
|
});
|
|
|
|
// Return immediately with 202 Accepted
|
|
this.setStatus(202);
|
|
return this.success({ message: 'Tracking accepted' });
|
|
}
|
|
}
|