Stage M5 and Stage M6

This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:23 +07:00
parent 7688657d3c
commit 624a6402bf
77 changed files with 4879 additions and 176 deletions
@@ -0,0 +1,87 @@
import { Body, Controller, Get, Inject, Param, Post, Query, Req, Res } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import type { Request, Response } from 'express';
import type { Cart, Locale, Order } from '@sport/types';
import { placeOrderSchema, type PlaceOrderInput } from '@sport/validation';
import { Public } from '@/common/decorators/public.decorator';
import { RequestLocale } from '@/common/i18n/locale.decorator';
import { ZodValidationPipe } from '@/common/pipes/zod-validation.pipe';
import { APP_CONFIG } from '@/config/app-config.module';
import type { AppConfig } from '@/config/configuration';
import { clearCartCookie, resolveCartToken, setCartCookie } from '@/modules/carts/public';
import { OrdersService } from '@/modules/orders/public';
import { CheckoutService } from './checkout.service';
/**
* Public because guest checkout is the default. Customer accounts arrive in M8
* and will attach an order to a customer, not gate the ability to place one.
*/
@ApiTags('checkout')
@Public()
@Controller('checkout')
export class CheckoutController {
constructor(
private readonly service: CheckoutService,
private readonly orders: OrdersService,
@Inject(APP_CONFIG) private readonly config: AppConfig,
) {}
@Get('quote')
@ApiOperation({ summary: 'The bag as it will be charged' })
quote(
@Req() request: Request,
@Res({ passthrough: true }) response: Response,
@RequestLocale() locale: Locale,
): Promise<Cart> {
const { token } = resolveCartToken(request);
setCartCookie(response, token, this.config.app.isProduction);
return this.service.quote(token, locale);
}
@Post('orders')
@ApiOperation({ summary: 'Place the order and reserve stock' })
async placeOrder(
@Body(new ZodValidationPipe(placeOrderSchema)) body: PlaceOrderInput,
@Req() request: Request,
@Res({ passthrough: true }) response: Response,
@RequestLocale() locale: Locale,
): Promise<Order> {
const { token } = resolveCartToken(request);
const order = await this.service.placeOrder(token, body, locale);
// The bag is gone, so the cookie naming it should go too — otherwise the
// next visit reads an empty cart under a stale token forever.
clearCartCookie(response, this.config.app.isProduction);
return order;
}
@Get('orders/lookup')
@ApiOperation({ summary: 'Find a placed order by number and email' })
lookup(@Query('orderNumber') orderNumber: string, @Query('email') email: string): Promise<Order> {
return this.orders.lookup(orderNumber ?? '', email ?? '');
}
/**
* Confirmation lookup by id — a capability URL.
*
* The id is a UUIDv7: not sequential, not enumerable, and not derivable from
* the order number. Holding it is the authorisation, which is what lets a
* guest see their own order without an account.
*
* It exists so the confirmation page need not carry an email address in its
* query string, where it would sit in browser history and ride along in the
* `Referer` of every outbound request the page makes.
*
* Declared after `orders/lookup` so that literal path never matches here.
*/
@Get('orders/:id')
@ApiOperation({ summary: 'A placed order, by its unguessable id' })
getById(@Param('id') id: string): Promise<Order> {
return this.orders.getById(id);
}
}