feat(F-048): completed feature

This commit is contained in:
chattie
2026-08-19 07:17:14 +02:00
parent 8ee1938af9
commit 835ab66eda
187 changed files with 12361 additions and 1065 deletions

View File

@@ -1,9 +1,11 @@
import type { FastifyInstance } from 'fastify';
import type { FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import { requireRole, type Authenticate } from '../../../shared/auth.js';
import { parseJson } from '../../../shared/http-input.js';
import { AppError } from '../../../shared/errors.js';
import { errorSchema } from '../../../shared/swagger.js';
import { PromotionServiceImpl } from '../application/promotion-service.js';
import type { Promotion } from '../domain/promotion.js';
import { PgPromotionRepository } from '../infrastructure/pg-promotion-repository.js';
@@ -32,7 +34,12 @@ export async function registerPromotionsRoutes(
const service = new PromotionServiceImpl(new PgPromotionRepository(deps.pool));
// List all promotions (admin)
app.get('/promotions', async (request, reply) => {
const listPromoSchema: FastifySchema = {
tags: ['Promotions'],
summary: 'List promotions (admin)',
response: { 401: errorSchema, 403: errorSchema },
};
app.get('/promotions', { schema: listPromoSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const items = await service.list();
@@ -40,7 +47,13 @@ export async function registerPromotionsRoutes(
});
// Create promotion (admin)
app.post('/promotions', async (request, reply) => {
const createPromoSchema: FastifySchema = {
tags: ['Promotions'],
summary: 'Create promotion (admin)',
body: { type: 'object' },
response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
};
app.post('/promotions', { schema: createPromoSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const input = parseJson(promotionBodySchema, request.body);
@@ -49,7 +62,14 @@ export async function registerPromotionsRoutes(
});
// Update promotion (admin)
app.patch('/promotions/:code', async (request, reply) => {
const patchPromoSchema: FastifySchema = {
tags: ['Promotions'],
summary: 'Update promotion (admin)',
params: { type: 'object', required: ['code'], properties: { code: { type: 'string' } } },
body: { type: 'object' },
response: { 401: errorSchema, 403: errorSchema, 404: errorSchema },
};
app.patch('/promotions/:code', { schema: patchPromoSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { code } = parseJson(z.object({ code: z.string() }), request.params);
@@ -60,12 +80,20 @@ export async function registerPromotionsRoutes(
} catch (error) {
throw error instanceof Error && error.message === 'Promotion not found'
? new AppError(404, 'NOT_FOUND', 'Promotion not found')
: (error instanceof Error ? error : new Error('Unknown error'));
: error instanceof Error
? error
: new Error('Unknown error');
}
});
// Delete promotion (admin)
app.delete('/promotions/:code', async (request, reply) => {
const deletePromoSchema: FastifySchema = {
tags: ['Promotions'],
summary: 'Delete promotion (admin)',
params: { type: 'object', required: ['code'], properties: { code: { type: 'string' } } },
response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema, 404: errorSchema },
};
app.delete('/promotions/:code', { schema: deletePromoSchema }, async (request, reply) => {
const user = await deps.authenticate(request);
requireRole(user, 'admin');
const { code } = parseJson(z.object({ code: z.string() }), request.params);

View File

@@ -1,4 +1,4 @@
import type { NewPromotion, Promotion, PromotionDiscount, PromotionType } from '../domain/promotion.js';
import type { NewPromotion, Promotion, PromotionDiscount } from '../domain/promotion.js';
import { PromotionInvalidError } from '../domain/errors.js';
import type { PromotionRepository, PromotionService } from '../domain/ports.js';

View File

@@ -1,7 +1,7 @@
/** Public API of the promotions module. */
import type pg from 'pg';
import { PromotionServiceImpl } from './application/promotion-service.js';
import type { PromotionService, PromotionRepository } from './domain/ports.js';
import type { PromotionService } from './domain/ports.js';
import { PgPromotionRepository } from './infrastructure/pg-promotion-repository.js';
export { registerPromotionsRoutes, type PromotionsRoutesDeps } from './api/promotions.routes.js';

View File

@@ -43,11 +43,9 @@ describe('PromotionService', () => {
),
).resolves.toMatchObject({ code: 'SAVE10', discountCents: 200 });
await expect(
new PromotionServiceImpl(repo({ ...PROMO, type: 'fixed_amount', value: 5000 })).calculateDiscount(
'x',
1200,
new Date('2026-06-01T00:00:00Z'),
),
new PromotionServiceImpl(
repo({ ...PROMO, type: 'fixed_amount', value: 5000 }),
).calculateDiscount('x', 1200, new Date('2026-06-01T00:00:00Z')),
).resolves.toMatchObject({ discountCents: 1200 });
});