From f775a62c647310161f3bdbdc352e28fbce9a5468 Mon Sep 17 00:00:00 2001 From: tema Date: Thu, 25 Dec 2025 17:51:02 +0900 Subject: [PATCH] Update pnpm-lock.yaml and Refactor BFF Order Management - Updated pnpm-lock.yaml to standardize quotes and improve consistency across dependencies. - Added @nestjs/swagger and swagger-ui-express to the BFF package.json for enhanced API documentation. - Refactored notifications service to utilize Prisma types for better type safety. - Removed orders controller, DTO, and orchestrator service to streamline order management functionality. - Enhanced next.config.mjs to support API proxying in development, improving local development experience. --- apps/bff/package.json | 2 + .../notifications/notifications.service.ts | 3 +- apps/bff/src/orders/dto/order.dto.ts | 123 ---- apps/bff/src/orders/orders.controller.ts | 71 --- .../services/order-orchestrator.service.ts | 315 ---------- apps/portal/next.config.mjs | 19 +- apps/portal/src/app/checkout/page.tsx | 584 ------------------ .../checkout/address-confirmation.tsx | 468 -------------- pnpm-lock.yaml | 71 ++- 9 files changed, 65 insertions(+), 1591 deletions(-) delete mode 100644 apps/bff/src/orders/dto/order.dto.ts delete mode 100644 apps/bff/src/orders/orders.controller.ts delete mode 100644 apps/bff/src/orders/services/order-orchestrator.service.ts delete mode 100644 apps/portal/src/app/checkout/page.tsx delete mode 100644 apps/portal/src/components/checkout/address-confirmation.tsx diff --git a/apps/bff/package.json b/apps/bff/package.json index cb2b7a39..ff907c92 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -39,6 +39,7 @@ "@nestjs/core": "^11.1.9", "@nestjs/platform-express": "^11.1.9", "@nestjs/schedule": "^6.1.0", + "@nestjs/swagger": "^11.2.3", "@prisma/adapter-pg": "^7.1.0", "@prisma/client": "^7.1.0", "@sendgrid/mail": "^8.1.6", @@ -59,6 +60,7 @@ "rxjs": "^7.8.2", "salesforce-pubsub-api-client": "^5.5.1", "ssh2-sftp-client": "^12.0.1", + "swagger-ui-express": "^5.0.1", "zod": "catalog:" }, "devDependencies": { diff --git a/apps/bff/src/modules/notifications/notifications.service.ts b/apps/bff/src/modules/notifications/notifications.service.ts index 0b08a0d2..b6616c8e 100644 --- a/apps/bff/src/modules/notifications/notifications.service.ts +++ b/apps/bff/src/modules/notifications/notifications.service.ts @@ -9,6 +9,7 @@ import { Injectable, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { PrismaService } from "@bff/infra/database/prisma.service.js"; +import type { Notification as PrismaNotification } from "@prisma/client"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { NOTIFICATION_SOURCE, @@ -163,7 +164,7 @@ export class NotificationService { ]); return { - notifications: notifications.map(n => this.mapToNotification(n)), + notifications: notifications.map((n: PrismaNotification) => this.mapToNotification(n)), unreadCount, total, }; diff --git a/apps/bff/src/orders/dto/order.dto.ts b/apps/bff/src/orders/dto/order.dto.ts deleted file mode 100644 index 6ff25775..00000000 --- a/apps/bff/src/orders/dto/order.dto.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - IsString, - IsArray, - IsIn, - IsNotEmpty, - IsOptional, - IsObject, - ValidateNested, -} from "class-validator"; -import { Type } from "class-transformer"; - -export class OrderConfigurations { - // Activation (All order types) - @IsOptional() - @IsIn(["Immediate", "Scheduled"]) - activationType?: "Immediate" | "Scheduled"; - - @IsOptional() - @IsString() - scheduledAt?: string; - - // Internet specific - @IsOptional() - @IsIn(["IPoE-BYOR", "IPoE-HGW", "PPPoE"]) - accessMode?: "IPoE-BYOR" | "IPoE-HGW" | "PPPoE"; - - // SIM specific - @IsOptional() - @IsIn(["eSIM", "Physical SIM"]) - simType?: "eSIM" | "Physical SIM"; - - @IsOptional() - @IsString() - eid?: string; // Required for eSIM - - // MNP/Porting - @IsOptional() - @IsString() - isMnp?: string; // "true" | "false" - - @IsOptional() - @IsString() - mnpNumber?: string; - - @IsOptional() - @IsString() - mnpExpiry?: string; - - @IsOptional() - @IsString() - mnpPhone?: string; - - @IsOptional() - @IsString() - mvnoAccountNumber?: string; - - @IsOptional() - @IsString() - portingLastName?: string; - - @IsOptional() - @IsString() - portingFirstName?: string; - - @IsOptional() - @IsString() - portingLastNameKatakana?: string; - - @IsOptional() - @IsString() - portingFirstNameKatakana?: string; - - @IsOptional() - @IsIn(["Male", "Female", "Corporate/Other"]) - portingGender?: "Male" | "Female" | "Corporate/Other"; - - @IsOptional() - @IsString() - portingDateOfBirth?: string; - - // Address (when address is updated during checkout) - @IsOptional() - @IsObject() - address?: { - street?: string | null; - streetLine2?: string | null; - city?: string | null; - state?: string | null; - postalCode?: string | null; - country?: string | null; - }; - - // VPN region is inferred from product VPN_Region__c field, no user input needed -} - -export class CreateOrderDto { - @IsString() - @IsNotEmpty() - @IsIn(["Internet", "SIM", "VPN", "Other"]) - orderType: "Internet" | "SIM" | "VPN" | "Other"; - - @IsArray() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - skus: string[]; - - @IsOptional() - @IsObject() - @ValidateNested() - @Type(() => OrderConfigurations) - configurations?: OrderConfigurations; -} - -// Interface for service layer (extends DTO with additional fields) -export interface CreateOrderBody extends Omit { - configurations?: OrderConfigurations; - opportunityId?: string; // Additional field for internal use -} - -export interface UserMapping { - sfAccountId: string; - whmcsClientId: number; -} diff --git a/apps/bff/src/orders/orders.controller.ts b/apps/bff/src/orders/orders.controller.ts deleted file mode 100644 index cf4647d2..00000000 --- a/apps/bff/src/orders/orders.controller.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Body, Controller, Get, Param, Post, Request } from "@nestjs/common"; -import { OrderOrchestrator } from "./services/order-orchestrator.service"; -import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { RequestWithUser } from "../auth/auth.types"; -import { Logger } from "nestjs-pino"; -import { CreateOrderDto } from "./dto/order.dto"; - -@ApiTags("orders") -@Controller("orders") -export class OrdersController { - constructor( - private orderOrchestrator: OrderOrchestrator, - private readonly logger: Logger - ) {} - - @ApiBearerAuth() - @Post() - @ApiOperation({ summary: "Create Salesforce Order" }) - @ApiResponse({ status: 201, description: "Order created successfully" }) - @ApiResponse({ status: 400, description: "Invalid request data" }) - async create(@Request() req: RequestWithUser, @Body() body: CreateOrderDto) { - this.logger.log( - { - userId: req.user?.id, - orderType: body.orderType, - skuCount: body.skus?.length || 0, - requestBody: JSON.stringify(body, null, 2), - }, - "Order creation request received" - ); - - try { - return await this.orderOrchestrator.createOrder(req.user.id, body); - } catch (error) { - this.logger.error( - { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - userId: req.user?.id, - orderType: body.orderType, - fullRequestBody: JSON.stringify(body, null, 2), - }, - "Order creation failed" - ); - throw error; - } - } - - @ApiBearerAuth() - @Get("user") - @ApiOperation({ summary: "Get user's orders" }) - async getUserOrders(@Request() req: RequestWithUser) { - return this.orderOrchestrator.getOrdersForUser(req.user.id); - } - - @ApiBearerAuth() - @Get(":sfOrderId") - @ApiOperation({ summary: "Get order summary/status" }) - @ApiParam({ name: "sfOrderId", type: String }) - async get(@Request() req: RequestWithUser, @Param("sfOrderId") sfOrderId: string) { - return this.orderOrchestrator.getOrder(sfOrderId); - } - - @ApiBearerAuth() - @Post(":sfOrderId/provision") - @ApiOperation({ summary: "Trigger provisioning for an approved order" }) - @ApiParam({ name: "sfOrderId", type: String }) - async provision(@Request() req: RequestWithUser, @Param("sfOrderId") sfOrderId: string) { - return this.orderOrchestrator.provisionOrder(req.user.id, sfOrderId); - } -} diff --git a/apps/bff/src/orders/services/order-orchestrator.service.ts b/apps/bff/src/orders/services/order-orchestrator.service.ts deleted file mode 100644 index 3f6b8bfd..00000000 --- a/apps/bff/src/orders/services/order-orchestrator.service.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { Injectable, Inject, BadRequestException } from "@nestjs/common"; -import { Logger } from "nestjs-pino"; -import { SalesforceConnection } from "../../vendors/salesforce/services/salesforce-connection.service"; -import { OrderValidator } from "./order-validator.service"; -import { OrderBuilder } from "./order-builder.service"; -import { OrderItemBuilder } from "./order-item-builder.service"; -import { - SalesforceOrder, - SalesforceOrderItem, - SalesforceQueryResult, -} from "../types/salesforce-order.types"; -import { getSalesforceFieldMap } from "../../common/config/field-map"; - -/** - * Main orchestrator for order operations - * Coordinates all order-related services - */ -@Injectable() -export class OrderOrchestrator { - constructor( - @Inject(Logger) private readonly logger: Logger, - private readonly sf: SalesforceConnection, - private readonly orderValidator: OrderValidator, - private readonly orderBuilder: OrderBuilder, - private readonly orderItemBuilder: OrderItemBuilder - ) {} - - /** - * Create a new order - main entry point - */ - async createOrder(userId: string, rawBody: unknown) { - this.logger.log({ userId }, "Order creation workflow started"); - - // 1) Complete validation (format + business rules) - const { validatedBody, userMapping, pricebookId } = - await this.orderValidator.validateCompleteOrder(userId, rawBody); - - this.logger.log( - { - userId, - orderType: validatedBody.orderType, - skuCount: validatedBody.skus.length, - }, - "Order validation completed successfully" - ); - - // 2) Build order fields (includes address snapshot) - const orderFields = await this.orderBuilder.buildOrderFields( - validatedBody, - userMapping, - pricebookId, - userId - ); - - this.logger.log({ orderType: orderFields.Type }, "Creating Salesforce Order"); - - // 4) Create Order in Salesforce - let created: { id: string }; - try { - this.logger.log( - { - orderFields: JSON.stringify(orderFields, null, 2), - fieldsCount: Object.keys(orderFields).length - }, - "About to create Salesforce Order with fields" - ); - - created = (await this.sf.sobject("Order").create(orderFields)) as { id: string }; - this.logger.log({ orderId: created.id }, "Salesforce Order created successfully"); - } catch (error) { - this.logger.error( - { - error: error instanceof Error ? error.message : String(error), - errorDetails: error, - stack: error instanceof Error ? error.stack : undefined, - orderType: orderFields.Type, - orderFields: JSON.stringify(orderFields, null, 2), - }, - "Failed to create Salesforce Order" - ); - throw error; - } - - // 5) Create OrderItems from SKUs - await this.orderItemBuilder.createOrderItemsFromSKUs( - created.id, - validatedBody.skus, - pricebookId - ); - - this.logger.log( - { - orderId: created.id, - skuCount: validatedBody.skus.length, - }, - "Order creation workflow completed successfully" - ); - - return { - sfOrderId: created.id, - status: "Created", - message: "Order created successfully in Salesforce", - }; - } - - /** - * Get order by ID with order items - */ - async getOrder(orderId: string) { - this.logger.log({ orderId }, "Fetching order details with items"); - - const fields = getSalesforceFieldMap(); - const orderSoql = ` - SELECT Id, OrderNumber, Status, ${fields.order.orderType}, EffectiveDate, TotalAmount, - Account.Name, CreatedDate, LastModifiedDate, - Activation_Type__c, Activation_Status__c, Activation_Scheduled_At__c, - WHMCS_Order_ID__c - FROM Order - WHERE Id = '${orderId}' - LIMIT 1 - `; - - const orderItemsSoql = ` - SELECT Id, OrderId, Quantity, UnitPrice, TotalPrice, - PricebookEntry.Id, - PricebookEntry.Product2.Id, - PricebookEntry.Product2.Name, - PricebookEntry.Product2.StockKeepingUnit, - PricebookEntry.Product2.WH_Product_ID__c, - PricebookEntry.Product2.Item_Class__c, - PricebookEntry.Product2.Billing_Cycle__c - FROM OrderItem - WHERE OrderId = '${orderId}' - ORDER BY CreatedDate ASC - `; - - try { - const [orderResult, itemsResult] = await Promise.all([ - this.sf.query(orderSoql) as Promise>, - this.sf.query(orderItemsSoql) as Promise>, - ]); - - const order = orderResult.records?.[0]; - - if (!order) { - this.logger.warn({ orderId }, "Order not found"); - return null; - } - - const orderItems = (itemsResult.records || []).map((item: SalesforceOrderItem) => ({ - id: item.Id, - quantity: item.Quantity, - unitPrice: item.UnitPrice, - totalPrice: item.TotalPrice, - product: { - id: item.PricebookEntry?.Product2?.Id, - name: item.PricebookEntry?.Product2?.Name, - sku: String(item.PricebookEntry?.Product2?.StockKeepingUnit || ""), // This is the key field that was missing! - whmcsProductId: String(item.PricebookEntry?.Product2?.WH_Product_ID__c || ""), - itemClass: String(item.PricebookEntry?.Product2?.Item_Class__c || ""), - billingCycle: String(item.PricebookEntry?.Product2?.Billing_Cycle__c || ""), - }, - })); - - this.logger.log( - { orderId, itemCount: orderItems.length }, - "Order details retrieved with items" - ); - - return { - id: order.Id, - orderNumber: order.OrderNumber, - status: order.Status, - orderType: order.Type, - effectiveDate: order.EffectiveDate, - totalAmount: order.TotalAmount, - accountName: order.Account?.Name, - createdDate: order.CreatedDate, - lastModifiedDate: order.LastModifiedDate, - activationType: order.Activation_Type__c, - activationStatus: order.Activation_Status__c, - scheduledAt: order.Activation_Scheduled_At__c, - whmcsOrderId: order.WHMCS_Order_ID__c, - items: orderItems, // Now includes all the product details with SKUs! - }; - } catch (error) { - this.logger.error({ error, orderId }, "Failed to fetch order with items"); - throw error; - } - } - - /** - * Get orders for a user with basic item summary - */ - async getOrdersForUser(userId: string) { - this.logger.log({ userId }, "Fetching user orders with item summaries"); - - // Get user mapping - const userMapping = await this.orderValidator.validateUserMapping(userId); - - const fields = getSalesforceFieldMap(); - const ordersSoql = ` - SELECT Id, OrderNumber, Status, ${fields.order.orderType}, EffectiveDate, TotalAmount, - CreatedDate, LastModifiedDate, WHMCS_Order_ID__c - FROM Order - WHERE AccountId = '${userMapping.sfAccountId}' - ORDER BY CreatedDate DESC - LIMIT 50 - `; - - try { - const ordersResult = (await this.sf.query( - ordersSoql - )) as SalesforceQueryResult; - const orders = ordersResult.records || []; - - if (orders.length === 0) { - return []; - } - - // Get order items for all orders in one query - const orderIds = orders.map(o => `'${o.Id}'`).join(","); - const itemsSoql = ` - SELECT Id, OrderId, Quantity, - PricebookEntry.Product2.Name, - PricebookEntry.Product2.StockKeepingUnit, - PricebookEntry.Product2.Item_Class__c - FROM OrderItem - WHERE OrderId IN (${orderIds}) - ORDER BY OrderId, CreatedDate ASC - `; - - const itemsResult = (await this.sf.query( - itemsSoql - )) as SalesforceQueryResult; - const allItems = itemsResult.records || []; - - // Group items by order ID - const itemsByOrder = allItems.reduce( - (acc, item: SalesforceOrderItem) => { - if (!acc[item.OrderId]) acc[item.OrderId] = []; - acc[item.OrderId].push({ - name: String(item.PricebookEntry?.Product2?.Name || ""), - sku: String(item.PricebookEntry?.Product2?.StockKeepingUnit || ""), - itemClass: String(item.PricebookEntry?.Product2?.Item_Class__c || ""), - quantity: item.Quantity, - }); - return acc; - }, - {} as Record< - string, - Array<{ - name?: string; - sku?: string; - itemClass?: string; - quantity: number; - }> - > - ); - - return orders.map((order: SalesforceOrder) => ({ - id: order.Id, - orderNumber: order.OrderNumber, - status: order.Status, - orderType: order.Type, - effectiveDate: order.EffectiveDate, - totalAmount: order.TotalAmount, - createdDate: order.CreatedDate, - lastModifiedDate: order.LastModifiedDate, - whmcsOrderId: order.WHMCS_Order_ID__c, - itemsSummary: itemsByOrder[order.Id] || [], // Include basic item info for order list - })); - } catch (error) { - this.logger.error({ error, userId }, "Failed to fetch user orders with items"); - throw error; - } - } - - /** - * Trigger provisioning for an approved order - */ - async provisionOrder(userId: string, orderId: string) { - this.logger.log({ userId, orderId }, "Triggering order provisioning"); - - // Get order and verify it belongs to the user - const order = await this.getOrder(orderId); - if (!order) { - this.logger.warn({ orderId, userId }, "Order not found for provisioning"); - throw new BadRequestException("Order not found"); - } - - // For now, just update the order status to indicate provisioning has started - // In a real implementation, this would trigger the actual provisioning workflow - const soql = ` - UPDATE Order - SET Status = 'Provisioning Started', - Provisioning_Status__c = 'In Progress' - WHERE Id = '${orderId}' - `; - - try { - await this.sf.query(soql); - this.logger.log({ orderId }, "Order provisioning triggered successfully"); - - return { - orderId, - status: "Provisioning Started", - message: "Order provisioning has been initiated", - }; - } catch (error) { - this.logger.error({ error, orderId }, "Failed to trigger order provisioning"); - throw error; - } - } -} diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs index cb2a54e4..69edd097 100644 --- a/apps/portal/next.config.mjs +++ b/apps/portal/next.config.mjs @@ -4,6 +4,10 @@ import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const workspaceRoot = path.resolve(__dirname, "..", ".."); +// BFF URL for development API proxying +const BFF_URL = process.env.BFF_URL || "http://localhost:4000"; +const isDev = process.env.NODE_ENV === "development"; + /** @type {import('next').NextConfig} */ const nextConfig = { output: process.env.NODE_ENV === "production" ? "standalone" : undefined, @@ -23,11 +27,24 @@ const nextConfig = { }, env: { - NEXT_PUBLIC_API_BASE: process.env.NEXT_PUBLIC_API_BASE, + // In development, we use rewrites to proxy API calls, so API_BASE is same-origin + // In production, API_BASE should be set via environment (nginx proxy or direct BFF URL) + NEXT_PUBLIC_API_BASE: isDev ? "" : process.env.NEXT_PUBLIC_API_BASE, NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION, }, + // Proxy API requests to BFF in development + async rewrites() { + if (!isDev) return []; + return [ + { + source: "/api/:path*", + destination: `${BFF_URL}/api/:path*`, + }, + ]; + }, + compiler: { removeConsole: process.env.NODE_ENV === "production", }, diff --git a/apps/portal/src/app/checkout/page.tsx b/apps/portal/src/app/checkout/page.tsx deleted file mode 100644 index 745c5426..00000000 --- a/apps/portal/src/app/checkout/page.tsx +++ /dev/null @@ -1,584 +0,0 @@ -"use client"; - -import { useState, useEffect, useMemo, useCallback, Suspense } from "react"; -import { useSearchParams, useRouter } from "next/navigation"; -import { PageLayout } from "@/components/layout/page-layout"; -import { ShieldCheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline"; -import { authenticatedApi } from "@/lib/api"; -import { AddressConfirmation } from "@/components/checkout/address-confirmation"; -import { usePaymentMethods } from "@/hooks/useInvoices"; - -import { - InternetPlan, - InternetAddon, - InternetInstallation, - SimPlan, - SimActivationFee, - SimAddon, - CheckoutState, - OrderItem, - buildInternetOrderItems, - buildSimOrderItems, - calculateTotals, - buildOrderSKUs, -} from "@/shared/types/catalog.types"; - -interface Address { - street: string | null; - streetLine2: string | null; - city: string | null; - state: string | null; - postalCode: string | null; - country: string | null; -} - -function CheckoutContent() { - const params = useSearchParams(); - const router = useRouter(); - const [submitting, setSubmitting] = useState(false); - const [addressConfirmed, setAddressConfirmed] = useState(false); - const [confirmedAddress, setConfirmedAddress] = useState
(null); - const [forceUpdate, setForceUpdate] = useState(0); - const [checkoutState, setCheckoutState] = useState({ - loading: true, - error: null, - orderItems: [], - totals: { monthlyTotal: 0, oneTimeTotal: 0 }, - }); - - // Fetch payment methods to check if user has payment method on file - const { data: paymentMethods, isLoading: paymentMethodsLoading, error: paymentMethodsError, refetch: refetchPaymentMethods } = usePaymentMethods(); - - const orderType = (() => { - const type = params.get("type") || "internet"; - // Map to backend expected values - switch (type.toLowerCase()) { - case "sim": - return "SIM"; - case "internet": - return "Internet"; - case "vpn": - return "VPN"; - default: - return "Other"; - } - })(); - - const selections = useMemo(() => { - const obj: Record = {}; - params.forEach((v, k) => { - if (k !== "type") obj[k] = v; - }); - return obj; - }, [params]); - - useEffect(() => { - let mounted = true; - void (async () => { - try { - setCheckoutState(prev => ({ ...prev, loading: true, error: null })); - - // Validate required parameters - if (!selections.plan) { - throw new Error("No plan selected. Please go back and select a plan."); - } - - let orderItems: OrderItem[] = []; - - if (orderType === "Internet") { - // Fetch Internet data - const [plans, addons, installations] = await Promise.all([ - authenticatedApi.get("/catalog/internet/plans"), - authenticatedApi.get("/catalog/internet/addons"), - authenticatedApi.get("/catalog/internet/installations"), - ]); - - const plan = plans.find(p => p.sku === selections.plan); - if (!plan) { - throw new Error( - `Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.` - ); - } - - // Handle addon SKUs like SIM flow - const addonSkus: string[] = []; - const urlParams = new URLSearchParams(window.location.search); - urlParams.getAll("addonSku").forEach(sku => { - if (sku && !addonSkus.includes(sku)) { - addonSkus.push(sku); - } - }); - - orderItems = buildInternetOrderItems(plan, addons, installations, { - installationSku: selections.installationSku, - addonSkus: addonSkus.length > 0 ? addonSkus : undefined, - }); - } else if (orderType === "SIM") { - // Fetch SIM data - const [plans, activationFees, addons] = await Promise.all([ - authenticatedApi.get("/catalog/sim/plans"), - authenticatedApi.get("/catalog/sim/activation-fees"), - authenticatedApi.get("/catalog/sim/addons"), - ]); - - const plan = plans.find(p => p.sku === selections.plan); // Look up by SKU instead of ID - if (!plan) { - throw new Error( - `SIM plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.` - ); - } - // Handle multiple addons from URL parameters - const addonSkus: string[] = []; - if (selections.addonSku) { - // Single addon (legacy support) - addonSkus.push(selections.addonSku); - } - // Check for multiple addonSku parameters - const urlParams = new URLSearchParams(window.location.search); - urlParams.getAll("addonSku").forEach(sku => { - if (sku && !addonSkus.includes(sku)) { - addonSkus.push(sku); - } - }); - - orderItems = buildSimOrderItems(plan, activationFees, addons, { - addonSkus: addonSkus.length > 0 ? addonSkus : undefined, - }); - } - - if (mounted) { - const totals = calculateTotals(orderItems); - setCheckoutState(prev => ({ - ...prev, - loading: false, - orderItems, - totals, - })); - } - } catch (error) { - if (mounted) { - setCheckoutState(prev => ({ - ...prev, - loading: false, - error: error instanceof Error ? error.message : "Failed to load checkout data", - })); - } - } - })(); - return () => { - mounted = false; - }; - }, [orderType, selections]); - - // Debug effect to track addressConfirmed changes - useEffect(() => { - console.log("🎯 PARENT: addressConfirmed state changed to:", addressConfirmed); - }, [addressConfirmed]); - - const handleSubmitOrder = async () => { - try { - setSubmitting(true); - - const skus = buildOrderSKUs(checkoutState.orderItems); - - // Validate we have SKUs before proceeding - if (!skus || skus.length === 0) { - throw new Error("No products selected for order. Please go back and select products."); - } - - // Additional validation for Internet orders - if (orderType === "Internet") { - const hasServicePlan = checkoutState.orderItems.some(item => item.type === "service"); - const hasInstallation = checkoutState.orderItems.some(item => item.type === "installation"); - - console.log("🔍 Internet order validation:", { - hasServicePlan, - hasInstallation, - orderItems: checkoutState.orderItems, - selections: selections - }); - - if (!hasServicePlan) { - throw new Error("Internet service plan is required. Please go back and select a plan."); - } - - // Installation is typically required for Internet orders - if (!hasInstallation) { - console.warn("⚠️ No installation selected for Internet order - this might cause issues"); - } - } - - // Send SKUs + configurations - backend resolves product data from SKUs, - // uses configurations for fields that cannot be inferred - const configurations: Record = {}; - - // Extract configurations from URL params (these come from configure pages) - if (selections.accessMode) configurations.accessMode = selections.accessMode; - if (selections.simType) configurations.simType = selections.simType; - if (selections.eid) configurations.eid = selections.eid; - // VPN region is inferred from product VPN_Region__c field, no configuration needed - if (selections.activationType) configurations.activationType = selections.activationType; - if (selections.scheduledAt) configurations.scheduledAt = selections.scheduledAt; - - // MNP fields (must match backend field expectations exactly) - if (selections.isMnp) configurations.isMnp = selections.isMnp; - if (selections.reservationNumber) configurations.mnpNumber = selections.reservationNumber; - if (selections.expiryDate) configurations.mnpExpiry = selections.expiryDate; - if (selections.phoneNumber) configurations.mnpPhone = selections.phoneNumber; - if (selections.mvnoAccountNumber) - configurations.mvnoAccountNumber = selections.mvnoAccountNumber; - if (selections.portingLastName) configurations.portingLastName = selections.portingLastName; - if (selections.portingFirstName) - configurations.portingFirstName = selections.portingFirstName; - if (selections.portingLastNameKatakana) - configurations.portingLastNameKatakana = selections.portingLastNameKatakana; - if (selections.portingFirstNameKatakana) - configurations.portingFirstNameKatakana = selections.portingFirstNameKatakana; - if (selections.portingGender) configurations.portingGender = selections.portingGender; - if (selections.portingDateOfBirth) - configurations.portingDateOfBirth = selections.portingDateOfBirth; - - // Include address in configurations if it was updated during checkout - if (confirmedAddress) { - configurations.address = confirmedAddress; - } - - const orderData = { - orderType, - skus: skus, - ...(Object.keys(configurations).length > 0 && { configurations }), - }; - - console.log("🚀 Submitting order with data:", JSON.stringify(orderData, null, 2)); - console.log("🚀 Address confirmed state:", addressConfirmed); - console.log("🚀 Confirmed address:", confirmedAddress); - console.log("🚀 Order type:", orderType); - console.log("🚀 SKUs:", skus); - - const response = await authenticatedApi.post<{ sfOrderId: string }>("/orders", orderData); - router.push(`/orders/${response.sfOrderId}?status=success`); - } catch (error) { - console.error("🚨 Order submission failed:", error); - - // Enhanced error logging for debugging - if (error instanceof Error) { - console.error("🚨 Error name:", error.name); - console.error("🚨 Error message:", error.message); - console.error("🚨 Error stack:", error.stack); - } - - // If it's an API error, try to get more details - if (error && typeof error === 'object' && 'status' in error) { - console.error("🚨 HTTP Status:", error.status); - console.error("🚨 Error details:", error); - } - - let errorMessage = "Order submission failed"; - if (error instanceof Error) { - errorMessage = error.message; - } - - setCheckoutState(prev => ({ - ...prev, - error: errorMessage, - })); - } finally { - setSubmitting(false); - } - }; - - const handleAddressConfirmed = useCallback((address?: Address) => { - console.log("🎯 PARENT: handleAddressConfirmed called with:", address); - console.log("🎯 PARENT: Current addressConfirmed state before:", addressConfirmed); - - console.log("🎯 PARENT: About to call setAddressConfirmed(true)..."); - setAddressConfirmed(prev => { - console.log("🎯 PARENT: setAddressConfirmed functional update - prev:", prev, "-> true"); - return true; - }); - console.log("🎯 PARENT: setAddressConfirmed(true) called"); - - console.log("🎯 PARENT: About to call setConfirmedAddress..."); - setConfirmedAddress(address || null); - console.log("🎯 PARENT: setConfirmedAddress called"); - - // Force a re-render to ensure the UI updates - setForceUpdate(prev => prev + 1); - console.log("🎯 PARENT: Force update triggered"); - }, [addressConfirmed]); - - const handleAddressIncomplete = () => { - setAddressConfirmed(false); - setConfirmedAddress(null); - }; - - if (checkoutState.loading) { - return ( - } - > -
Loading order submission...
-
- ); - } - - if (checkoutState.error) { - return ( - } - > -
-

{checkoutState.error}

- -
-
- ); - } - - return ( - } - > -
- {/* Address Confirmation */} - - - {/* Order Submission Message */} -
-
- -
-

Submit Your Order for Review

-

- You've configured your service and reviewed all details. Your order will be - submitted for review and approval. -

-
-

What happens next?

-
-

• Your order will be reviewed by our team

-

• We'll set up your services in our system

-

• Payment will be processed using your card on file

-

• You'll receive confirmation once everything is ready

-
-
- - {/* Quick Totals Summary */} -
-
- Total: -
-
- ¥{checkoutState.totals.monthlyTotal.toLocaleString()}/mo -
- {checkoutState.totals.oneTimeTotal > 0 && ( -
- + ¥{checkoutState.totals.oneTimeTotal.toLocaleString()} one-time -
- )} -
-
-
-
- -
-

Billing Information

- {paymentMethodsLoading ? ( -
-
-
- Checking payment methods... -
-
- ) : paymentMethodsError ? ( -
-
- -
-

Unable to verify payment methods

-

- We couldn't check your payment methods. If you just added a payment method, try refreshing. -

-
- - -
-
-
-
- ) : paymentMethods && paymentMethods.paymentMethods.length > 0 ? ( -
-
-
- - - -
-
-

Payment method verified

-

- After order approval, payment will be automatically processed using your existing - payment method on file. No additional payment steps required. -

-
-
-
- ) : ( -
-
- -
-

No payment method on file

-

- You need to add a payment method before submitting your order. Please add a credit card or other payment method to proceed. -

- -
-
-
- )} -
- - {/* Debug Info - Remove in production */} -
- Debug Info: Address Confirmed: {addressConfirmed ? '✅ TRUE' : '❌ FALSE'} | - Order Type: {orderType} | - Order Items: {checkoutState.orderItems.length} | - Payment Methods: {paymentMethodsLoading ? '⏳ Loading...' : paymentMethodsError ? '❌ Error' : paymentMethods ? `✅ ${paymentMethods.paymentMethods.length} found` : '❌ None'} | - Force Update: {forceUpdate} | - Can Submit: {!( - submitting || - checkoutState.orderItems.length === 0 || - !addressConfirmed || - paymentMethodsLoading || - !paymentMethods || - paymentMethods.paymentMethods.length === 0 - ) ? '✅ YES' : '❌ NO'} -
- -
- - - -
-
-
- ); -} - -export default function CheckoutPage() { - return ( - Loading checkout...}> - - - ); -} diff --git a/apps/portal/src/components/checkout/address-confirmation.tsx b/apps/portal/src/components/checkout/address-confirmation.tsx deleted file mode 100644 index 71f6bcbd..00000000 --- a/apps/portal/src/components/checkout/address-confirmation.tsx +++ /dev/null @@ -1,468 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { authenticatedApi } from "@/lib/api"; -import { - MapPinIcon, - PencilIcon, - CheckIcon, - XMarkIcon, - ExclamationTriangleIcon, -} from "@heroicons/react/24/outline"; - -interface Address { - street: string | null; - streetLine2: string | null; - city: string | null; - state: string | null; - postalCode: string | null; - country: string | null; -} - -interface BillingInfo { - company: string | null; - email: string; - phone: string | null; - address: Address; - isComplete: boolean; -} - -interface AddressConfirmationProps { - onAddressConfirmed: (address?: Address) => void; - onAddressIncomplete: () => void; - orderType?: string; // Add order type to customize behavior - // Optional controlled props for parent state management - addressConfirmed?: boolean; // If provided, use this instead of internal state - onAddressConfirmationChange?: (confirmed: boolean) => void; // Callback for controlled mode -} - -export function AddressConfirmation({ - onAddressConfirmed, - onAddressIncomplete, - orderType, - addressConfirmed: controlledAddressConfirmed, - onAddressConfirmationChange, -}: AddressConfirmationProps) { - const [billingInfo, setBillingInfo] = useState(null); - const [loading, setLoading] = useState(true); - const [editing, setEditing] = useState(false); - const [editedAddress, setEditedAddress] = useState
(null); - const [error, setError] = useState(null); - const [internalAddressConfirmed, setInternalAddressConfirmed] = useState(false); - - // Use controlled prop if provided, otherwise use internal state - const addressConfirmed = controlledAddressConfirmed ?? internalAddressConfirmed; - const setAddressConfirmed = (value: boolean | ((prev: boolean) => boolean)) => { - const newValue = typeof value === 'function' ? value(addressConfirmed) : value; - - if (controlledAddressConfirmed !== undefined && onAddressConfirmationChange) { - // Controlled mode: notify parent - onAddressConfirmationChange(newValue); - } else { - // Uncontrolled mode: update internal state - setInternalAddressConfirmed(newValue); - } - }; - - const isInternetOrder = orderType === "Internet"; - const requiresAddressVerification = isInternetOrder; - - const fetchBillingInfo = useCallback(async () => { - try { - setLoading(true); - const data = await authenticatedApi.get("/me/billing"); - setBillingInfo(data); - - // Since address is required at signup, it should always be complete - // But we still need verification for Internet orders - if (requiresAddressVerification) { - // For Internet orders, only reset confirmation state if not already confirmed - // This prevents clobbering existing confirmation on re-renders/re-fetches - if (!addressConfirmed) { - console.log("🏠 Internet order: Setting initial unconfirmed state"); - setAddressConfirmed(false); - onAddressIncomplete(); // Keep disabled until explicitly confirmed - } else { - console.log("🏠 Internet order: Preserving existing confirmation state"); - // Address is already confirmed, don't clobber the state - } - } else { - // For other order types, auto-confirm since address exists from signup - // Only call parent callback if we're not already confirmed to avoid spam - if (!addressConfirmed) { - console.log("🏠 Non-Internet order: Auto-confirming address"); - onAddressConfirmed(data.address); - setAddressConfirmed(true); - } else { - console.log("🏠 Non-Internet order: Already confirmed, skipping callback"); - } - } - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to load address"); - } finally { - setLoading(false); - } - }, [requiresAddressVerification, onAddressIncomplete, onAddressConfirmed, addressConfirmed]); - - useEffect(() => { - void fetchBillingInfo(); - }, [fetchBillingInfo]); - - const handleEdit = () => { - setEditing(true); - setEditedAddress( - billingInfo?.address || { - street: "", - streetLine2: "", - city: "", - state: "", - postalCode: "", - country: "", - } - ); - }; - - const handleSave = () => { - if (!editedAddress) return; - - // Validate required fields - const isComplete = !!( - editedAddress.street?.trim() && - editedAddress.city?.trim() && - editedAddress.state?.trim() && - editedAddress.postalCode?.trim() && - editedAddress.country?.trim() - ); - - if (!isComplete) { - setError("Please fill in all required address fields"); - return; - } - - try { - setError(null); - - // UX-FIRST: Update UI immediately - setEditing(false); - setAddressConfirmed(true); - - // Update local state to show the new address - if (billingInfo) { - setBillingInfo({ - ...billingInfo, - address: editedAddress, - isComplete: true, - }); - } - - // SIDE-EFFECT SECOND: Use the edited address for the order (will be flagged as changed) - onAddressConfirmed(editedAddress); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to update address"); - } - }; - - const handleConfirmAddress = (e: React.MouseEvent) => { - // Prevent any default behavior and event propagation - e.preventDefault(); - e.stopPropagation(); - - console.log("🏠 CONFIRM ADDRESS CLICKED", { - billingInfo, - hasAddress: !!billingInfo?.address, - address: billingInfo?.address, - currentAddressConfirmed: addressConfirmed - }); - - if (billingInfo?.address) { - console.log("🏠 UX-First approach: Updating local state immediately for instant UI feedback"); - - // UX-FIRST: Update local state immediately for instant UI response - setAddressConfirmed(true); - console.log("🏠 ✅ Local addressConfirmed set to true (UI will update immediately)"); - - // SIDE-EFFECT SECOND: Notify parent after local state update - console.log("🏠 Notifying parent component..."); - onAddressConfirmed(billingInfo.address); - console.log("🏠 ✅ Parent onAddressConfirmed() called with:", billingInfo.address); - } else { - console.log("🏠 ❌ No billing info or address available"); - } - }; - - const handleCancel = () => { - setEditing(false); - setEditedAddress(null); - setError(null); - }; - - if (loading) { - return ( -
-
-
- Loading address information... -
-
- ); - } - - if (error) { - return ( -
-
- -
-

Address Error

-

{error}

- -
-
-
- ); - } - - if (!billingInfo) return null; - - return ( -
- {/* Debug Info - Remove in production */} -
- AddressConfirmation Debug: isInternetOrder: {isInternetOrder ? '✅' : '❌'} | - addressConfirmed: {addressConfirmed ? '✅' : '❌'} | - controlledMode: {controlledAddressConfirmed !== undefined ? '✅' : '❌'} | - billingInfo: {billingInfo ? '✅' : '❌'} | - hasAddress: {billingInfo?.address ? '✅' : '❌'} | - showConfirmButton: {(isInternetOrder && !addressConfirmed) ? '✅' : '❌'} -
- -
-
- -

- {isInternetOrder - ? "Verify Installation Address" - : billingInfo.isComplete - ? "Confirm Service Address" - : "Complete Your Address"} -

-
- {billingInfo.isComplete && !editing && ( - - )} -
- - {/* Address should always be complete since it's required at signup */} - - {isInternetOrder && !addressConfirmed && ( -
-
- -
-

- Internet Installation Address Verification Required -

-

- Please verify this is the correct address for your internet installation. A - technician will visit this location for setup. -

-
-
-
- )} - - {editing ? ( -
-
- - { - setError(null); // Clear error on input - setEditedAddress(prev => (prev ? { ...prev, street: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - placeholder="123 Main Street" - required - /> -
- -
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, streetLine2: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Apartment, suite, etc. (optional)" - /> -
- -
-
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, city: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Tokyo" - /> -
- -
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, state: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="Tokyo" - /> -
- -
- - { - setError(null); - setEditedAddress(prev => (prev ? { ...prev, postalCode: e.target.value } : null)); - }} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - placeholder="100-0001" - /> -
-
- -
- - -
- -
- - -
-
- ) : ( -
- {billingInfo.address.street ? ( -
-
-

{billingInfo.address.street}

- {billingInfo.address.streetLine2 &&

{billingInfo.address.streetLine2}

} -

- {billingInfo.address.city}, {billingInfo.address.state}{" "} - {billingInfo.address.postalCode} -

-

{billingInfo.address.country}

-
- - {/* Address Confirmation for Internet Orders */} - {isInternetOrder && !addressConfirmed && ( -
-
-
- - - Verification Required - -
- -
-
- )} - - {/* Address Confirmed Status */} - {addressConfirmed && ( -
-
- - - {isInternetOrder ? "Installation Address Confirmed" : "Address Confirmed"} - -
-
- )} -
- ) : ( -
- -

No address on file

- -
- )} -
- )} -
- ); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b8dbf96..c5486b87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: "@nestjs/schedule": specifier: ^6.1.0 version: 6.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + "@nestjs/swagger": + specifier: ^11.2.3 + version: 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) "@prisma/adapter-pg": specifier: ^7.1.0 version: 7.1.0 @@ -118,7 +121,7 @@ importers: version: 4.5.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) nestjs-zod: specifier: ^5.0.1 - version: 5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.13) + version: 5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.13) p-queue: specifier: ^9.0.1 version: 9.0.1 @@ -143,6 +146,9 @@ importers: ssh2-sftp-client: specifier: ^12.0.1 version: 12.0.1 + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@5.1.0) zod: specifier: "catalog:" version: 4.1.13 @@ -1376,10 +1382,10 @@ packages: } engines: { node: ">=8" } - "@microsoft/tsdoc@0.15.1": + "@microsoft/tsdoc@0.16.0": resolution: { - integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==, + integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==, } "@mrleebo/prisma-ast@0.12.1": @@ -1720,10 +1726,10 @@ packages: peerDependencies: typescript: ">=4.8.2" - "@nestjs/swagger@11.2.0": + "@nestjs/swagger@11.2.3": resolution: { - integrity: sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==, + integrity: sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==, } peerDependencies: "@fastify/static": ^8.0.0 @@ -5828,13 +5834,6 @@ packages: } engines: { node: 20 || >=22 } - path-to-regexp@8.2.0: - resolution: - { - integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==, - } - engines: { node: ">=16" } - path-to-regexp@8.3.0: resolution: { @@ -6861,6 +6860,21 @@ packages: integrity: sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==, } + swagger-ui-dist@5.30.2: + resolution: + { + integrity: sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==, + } + + swagger-ui-express@5.0.1: + resolution: + { + integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==, + } + engines: { node: ">= v0.10.32" } + peerDependencies: + express: ">=4.0.0 || >=5.0.0-beta" + symbol-observable@4.0.0: resolution: { @@ -8126,8 +8140,7 @@ snapshots: "@lukeed/csprng@1.1.0": {} - "@microsoft/tsdoc@0.15.1": - optional: true + "@microsoft/tsdoc@0.16.0": {} "@mrleebo/prisma-ast@0.12.1": dependencies: @@ -8311,7 +8324,6 @@ snapshots: optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.2 - optional: true "@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)": dependencies: @@ -8342,21 +8354,20 @@ snapshots: transitivePeerDependencies: - chokidar - "@nestjs/swagger@11.2.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)": + "@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)": dependencies: - "@microsoft/tsdoc": 0.15.1 + "@microsoft/tsdoc": 0.16.0 "@nestjs/common": 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) "@nestjs/core": 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) "@nestjs/mapped-types": 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) js-yaml: 4.1.1 lodash: 4.17.21 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 reflect-metadata: 0.2.2 - swagger-ui-dist: 5.21.0 + swagger-ui-dist: 5.30.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.2 - optional: true "@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)": dependencies: @@ -8535,8 +8546,7 @@ snapshots: "@protobufjs/utf8@1.1.0": {} - "@scarf/scarf@1.4.0": - optional: true + "@scarf/scarf@1.4.0": {} "@sendgrid/client@8.1.6": dependencies: @@ -10698,14 +10708,14 @@ snapshots: pino-http: 11.0.0 rxjs: 7.8.2 - nestjs-zod@5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.13): + nestjs-zod@5.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.1.13): dependencies: "@nestjs/common": 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) deepmerge: 4.3.1 rxjs: 7.8.2 zod: 4.1.13 optionalDependencies: - "@nestjs/swagger": 11.2.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + "@nestjs/swagger": 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: @@ -10863,9 +10873,6 @@ snapshots: lru-cache: 11.2.4 minipass: 7.1.2 - path-to-regexp@8.2.0: - optional: true - path-to-regexp@8.3.0: {} path-type@4.0.0: {} @@ -11498,7 +11505,15 @@ snapshots: swagger-ui-dist@5.21.0: dependencies: "@scarf/scarf": 1.4.0 - optional: true + + swagger-ui-dist@5.30.2: + dependencies: + "@scarf/scarf": 1.4.0 + + swagger-ui-express@5.0.1(express@5.1.0): + dependencies: + express: 5.1.0 + swagger-ui-dist: 5.21.0 symbol-observable@4.0.0: {}