Assist_Design/apps/bff/src/modules/orders/orders.controller.ts

170 lines
5.5 KiB
TypeScript
Raw Normal View History

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";
2025-08-27 10:54:05 +09:00
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(),
});
2025-08-21 15:24:40 +09:00
@Controller("orders")
@UseGuards(RateLimitGuard)
export class OrdersController {
2025-08-27 10:54:05 +09:00
constructor(
2025-08-27 20:01:46 +09:00
private orderOrchestrator: OrderOrchestrator,
private readonly checkoutService: CheckoutService,
private readonly checkoutSessions: CheckoutSessionService,
private readonly orderEvents: OrderEventsService,
2025-08-27 10:54:05 +09:00
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) {
2025-08-28 16:57:57 +09:00
this.logger.log(
{
userId: req.user?.id,
orderType: body.orderType,
skuCount: body.skus?.length || 0,
2025-08-28 16:57:57 +09:00
},
"Order creation request received"
);
2025-08-27 20:01:46 +09:00
try {
const result = await this.orderOrchestrator.createOrder(req.user.id, body);
return this.createOrderResponseSchema.parse({ success: true, data: result });
2025-08-27 20:01:46 +09:00
} catch (error) {
2025-08-28 16:57:57 +09:00
this.logger.error(
{
error: error instanceof Error ? error.message : String(error),
userId: req.user?.id,
orderType: body.orderType,
2025-08-28 16:57:57 +09:00
},
"Order creation failed"
);
2025-08-27 20:01:46 +09:00
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 });
}
2025-08-27 20:01:46 +09:00
@Get("user")
@UseGuards(SalesforceReadThrottleGuard)
2025-08-27 20:01:46 +09:00
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<Observable<MessageEvent>> {
// 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
}