Stage M5 and Stage M6
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user