Assist_Design/apps/bff/src/orders/controllers/order-fulfillment.controller.ts

151 lines
5.0 KiB
TypeScript
Raw Normal View History

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 order 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;
}
}
// Removed /provision alias to avoid confusion — use /fulfill only
}