- Implemented FormStep component for user input (name, email, address). - Created OtpStep component for OTP verification. - Developed SuccessStep component to display success messages based on account creation. - Introduced eligibility-check.store for managing state throughout the eligibility check process. - Added commitlint configuration for standardized commit messages. - Configured knip for workspace management and project structure.
174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
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<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
|
|
}
|