import { Body, Controller, Get, NotFoundException, Param, Post, Request, Sse, UseGuards, UnauthorizedException, type MessageEvent, } from "@nestjs/common"; import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js"; import { OrderOrchestrator } from "./services/order-orchestrator.service.js"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { Logger } from "nestjs-pino"; import { createZodDto, ZodResponse } from "nestjs-zod"; import { checkoutSessionCreateOrderRequestSchema, createOrderRequestSchema, orderCreateResponseSchema, sfOrderIdParamSchema, orderDetailsSchema, orderListResponseSchema, type CreateOrderRequest, } from "@customer-portal/domain/orders"; import { Observable } from "rxjs"; import { OrderEventsService } from "./services/order-events.service.js"; import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.js"; import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard.js"; import { CheckoutService } from "./services/checkout.service.js"; import { CheckoutSessionService } from "./services/checkout-session.service.js"; class CreateOrderRequestDto extends createZodDto(createOrderRequestSchema) {} class CheckoutSessionCreateOrderDto extends createZodDto(checkoutSessionCreateOrderRequestSchema) {} class SfOrderIdParamDto extends createZodDto(sfOrderIdParamSchema) {} class CreateOrderResponseDto extends createZodDto(orderCreateResponseSchema) {} class OrderDetailsDto extends createZodDto(orderDetailsSchema) {} class OrderListResponseDto extends createZodDto(orderListResponseSchema) {} @Controller("orders") @UseGuards(RateLimitGuard) export class OrdersController { constructor( private orderOrchestrator: OrderOrchestrator, private readonly checkoutService: CheckoutService, private readonly checkoutSessions: CheckoutSessionService, private readonly orderEvents: OrderEventsService, private readonly logger: Logger ) {} @Post() @UseGuards(SalesforceWriteThrottleGuard) @RateLimit({ limit: 5, ttl: 60 }) // 5 order creations per minute @ZodResponse({ status: 201, description: "Create order", type: CreateOrderResponseDto }) async create(@Request() req: RequestWithUser, @Body() body: CreateOrderRequestDto) { this.logger.log( { userId: req.user?.id, orderType: body.orderType, skuCount: body.skus?.length || 0, }, "Order creation request received" ); try { const result = await this.orderOrchestrator.createOrder(req.user.id, body); return result; } catch (error) { this.logger.error( { error: error instanceof Error ? error.message : String(error), userId: req.user?.id, orderType: body.orderType, }, "Order creation failed" ); throw error; } } @Post("from-checkout-session") @UseGuards(SalesforceWriteThrottleGuard) @RateLimit({ limit: 5, ttl: 60 }) // 5 order creations per minute @ZodResponse({ status: 201, description: "Create order from checkout session", type: CreateOrderResponseDto, }) async createFromCheckoutSession( @Request() req: RequestWithUser, @Body() body: CheckoutSessionCreateOrderDto ) { this.logger.log( { userId: req.user?.id, checkoutSessionId: body.checkoutSessionId, }, "Order creation from checkout session request received" ); const session = await this.checkoutSessions.getSession(body.checkoutSessionId); const cart = await this.checkoutService.buildCart( session.request.orderType, session.request.selections, session.request.configuration, req.user?.id ); const uniqueSkus = [ ...new Set( cart.items .map(item => item.sku) .filter((sku): sku is string => typeof sku === "string" && sku.trim().length > 0) ), ]; if (uniqueSkus.length === 0) { throw new NotFoundException("Checkout session contains no items"); } const orderBody: CreateOrderRequest = { orderType: session.request.orderType, skus: uniqueSkus, ...(Object.keys(cart.configuration ?? {}).length > 0 ? { configurations: cart.configuration } : {}), }; const result = await this.orderOrchestrator.createOrder(req.user.id, orderBody); await this.checkoutSessions.deleteSession(body.checkoutSessionId); return result; } @Get("user") @UseGuards(SalesforceReadThrottleGuard) @ZodResponse({ description: "Get user orders", type: OrderListResponseDto }) async getUserOrders(@Request() req: RequestWithUser) { return this.orderOrchestrator.getOrdersForUser(req.user.id); } @Get(":sfOrderId") @UseGuards(SalesforceReadThrottleGuard) @ZodResponse({ description: "Get order details", type: OrderDetailsDto }) async get(@Request() req: RequestWithUser, @Param() params: SfOrderIdParamDto) { if (!req.user?.id) { throw new UnauthorizedException("Authentication required"); } return this.orderOrchestrator.getOrderForUser(params.sfOrderId, req.user.id); } @Sse(":sfOrderId/events") @UseGuards(SalesforceReadThrottleGuard) async streamOrderUpdates( @Request() req: RequestWithUser, @Param() params: SfOrderIdParamDto ): Promise> { // Ensure caller is allowed to access this order stream (avoid leaking existence) try { await this.orderOrchestrator.getOrderForUser(params.sfOrderId, req.user.id); } catch { throw new NotFoundException("Order not found"); } return this.orderEvents.subscribe(params.sfOrderId); } // Note: Order provisioning has been moved to SalesforceProvisioningController // This controller now focuses only on customer-facing order operations }