import { Body, Controller, Get, NotFoundException, Param, Post, Request, Sse, UsePipes, 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 { ZodValidationPipe } from "nestjs-zod"; import { createOrderRequestSchema, orderCreateResponseSchema, sfOrderIdParamSchema, type CreateOrderRequest, type SfOrderIdParam, } from "@customer-portal/domain/orders"; import { apiSuccessResponseSchema } from "@customer-portal/domain/common"; 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"; import { z } from "zod"; const checkoutSessionCreateOrderSchema = z.object({ checkoutSessionId: z.string().uuid(), }); @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 ) {} private readonly createOrderResponseSchema = apiSuccessResponseSchema(orderCreateResponseSchema); @Post() @UseGuards(SalesforceWriteThrottleGuard) @RateLimit({ limit: 5, ttl: 60 }) // 5 order creations per minute @UsePipes(new ZodValidationPipe(createOrderRequestSchema)) async create(@Request() req: RequestWithUser, @Body() body: CreateOrderRequest) { 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 this.createOrderResponseSchema.parse({ success: true, data: 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 @UsePipes(new ZodValidationPipe(checkoutSessionCreateOrderSchema)) async createFromCheckoutSession( @Request() req: RequestWithUser, @Body() body: { checkoutSessionId: string } ) { 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 = Array.from( 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 this.createOrderResponseSchema.parse({ success: true, data: result }); } @Get("user") @UseGuards(SalesforceReadThrottleGuard) async getUserOrders(@Request() req: RequestWithUser) { return this.orderOrchestrator.getOrdersForUser(req.user.id); } @Get(":sfOrderId") @UsePipes(new ZodValidationPipe(sfOrderIdParamSchema)) @UseGuards(SalesforceReadThrottleGuard) async get(@Request() req: RequestWithUser, @Param() params: SfOrderIdParam) { if (!req.user?.id) { throw new UnauthorizedException("Authentication required"); } return this.orderOrchestrator.getOrderForUser(params.sfOrderId, req.user.id); } @Sse(":sfOrderId/events") @UsePipes(new ZodValidationPipe(sfOrderIdParamSchema)) @UseGuards(SalesforceReadThrottleGuard) async streamOrderUpdates( @Request() req: RequestWithUser, @Param() params: SfOrderIdParam ): 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 }