import { Controller, Post, Param, Body, Headers, HttpCode, HttpStatus, UseGuards, } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiParam, ApiResponse, ApiHeader } from "@nestjs/swagger"; import { ThrottlerGuard } from "@nestjs/throttler"; import { Logger } from "nestjs-pino"; import { Public } from "../../auth/decorators/public.decorator"; import { EnhancedWebhookSignatureGuard } from "../../webhooks/guards/enhanced-webhook-signature.guard"; import { OrderFulfillmentService } from "../services/order-fulfillment.service"; import type { OrderFulfillmentRequest } from "../services/order-fulfillment.service"; @ApiTags("order-fulfillment") @Controller("orders") @Public() // Salesforce webhook uses signature-based auth, not JWT export class OrderFulfillmentController { constructor( private readonly orderFulfillmentService: OrderFulfillmentService, private readonly logger: Logger ) {} @Post(":sfOrderId/fulfill") @HttpCode(HttpStatus.OK) @UseGuards(ThrottlerGuard, EnhancedWebhookSignatureGuard) @ApiOperation({ summary: "Fulfill order from Salesforce", description: "Secure endpoint called by Salesforce Quick Action to fulfill orders in WHMCS. Handles complete flow: SF Order → WHMCS AddOrder/AcceptOrder → SF Status Update", }) @ApiParam({ name: "sfOrderId", type: String, description: "Salesforce Order ID to provision", example: "8014x000000ABCDXYZ", }) @ApiHeader({ name: "X-SF-Signature", description: "HMAC-SHA256 signature of request body using shared secret", required: true, example: "a1b2c3d4e5f6...", }) @ApiHeader({ name: "X-SF-Timestamp", description: "ISO timestamp of request (max 5 minutes old)", required: true, example: "2024-01-15T10:30:00Z", }) @ApiHeader({ name: "X-SF-Nonce", description: "Unique nonce to prevent replay attacks", required: true, example: "abc123def456", }) @ApiHeader({ name: "Idempotency-Key", description: "Unique key for safe retries", required: true, example: "provision_8014x000000ABCDXYZ_1705312200000", }) @ApiResponse({ status: 200, description: "Order provisioning completed successfully", schema: { type: "object", properties: { success: { type: "boolean", example: true }, status: { type: "string", enum: ["Provisioned", "Already Provisioned"], example: "Provisioned", }, whmcsOrderId: { type: "string", example: "12345" }, whmcsServiceIds: { type: "array", items: { type: "number" }, example: [67890, 67891] }, message: { type: "string", example: "Order provisioned successfully in WHMCS" }, }, }, }) @ApiResponse({ status: 400, description: "Invalid request or order not found", schema: { type: "object", properties: { success: { type: "boolean", example: false }, status: { type: "string", example: "Failed" }, message: { type: "string", example: "Salesforce order not found" }, errorCode: { type: "string", example: "ORDER_NOT_FOUND" }, }, }, }) @ApiResponse({ status: 401, description: "Invalid signature or authentication", }) @ApiResponse({ status: 409, description: "Payment method missing or other conflict", schema: { type: "object", properties: { success: { type: "boolean", example: false }, status: { type: "string", example: "Failed" }, message: { type: "string", example: "Payment method missing - client must add payment method before provisioning", }, errorCode: { type: "string", example: "PAYMENT_METHOD_MISSING" }, }, }, }) async fulfillOrder( @Param("sfOrderId") sfOrderId: string, @Body() payload: OrderFulfillmentRequest, @Headers("idempotency-key") idempotencyKey: string ) { this.logger.log("Salesforce fulfillment request received", { sfOrderId, idempotencyKey, timestamp: payload.timestamp, hasNonce: Boolean(payload.nonce), }); try { const result = await this.orderFulfillmentService.fulfillOrder( sfOrderId, payload, idempotencyKey ); this.logger.log("Salesforce provisioning completed", { sfOrderId, success: result.success, status: result.status, whmcsOrderId: result.whmcsOrderId, serviceCount: result.whmcsServiceIds?.length || 0, }); return { success: result.success, status: result.status, whmcsOrderId: result.whmcsOrderId, whmcsServiceIds: result.whmcsServiceIds, message: result.message, ...(result.errorCode && { errorCode: result.errorCode }), timestamp: new Date().toISOString(), }; } catch (error) { this.logger.error("Salesforce provisioning failed", { error: error instanceof Error ? error.message : String(error), sfOrderId, idempotencyKey, }); // Re-throw to let global exception handler format the response throw error; } } }