From fcc9bc247e7d005f1f7445b25b8f49d522e81389 Mon Sep 17 00:00:00 2001 From: barsa Date: Fri, 26 Dec 2025 13:40:10 +0900 Subject: [PATCH] Refactor Controllers to Utilize Zod DTOs for Parameter Validation - Updated InvoicesController, NotificationsController, OrdersController, and SubscriptionsController to replace inline parameter validation with Zod DTOs, enhancing code maintainability and clarity. - Introduced new DTOs for invoice and notification ID parameters, ensuring consistent validation across endpoints. - Refactored service method calls to utilize the new DTOs, improving type safety and reducing potential errors. - Cleaned up unused imports and optimized code structure for better readability. --- .../modules/invoices/invoices.controller.ts | 28 +-- .../notifications/notifications.controller.ts | 10 +- .../orders/controllers/checkout.controller.ts | 45 ++-- .../src/modules/orders/orders.controller.ts | 9 +- .../internet-eligibility.controller.ts | 22 +- .../subscriptions/subscriptions.controller.ts | 206 +++++++++++------- packages/domain/billing/index.ts | 1 + packages/domain/billing/schema.ts | 9 + packages/domain/notifications/index.ts | 2 + packages/domain/notifications/schema.ts | 9 + packages/domain/orders/schema.ts | 19 +- packages/domain/services/index.ts | 2 + packages/domain/services/schema.ts | 7 + packages/domain/sim/index.ts | 2 + packages/domain/sim/schema.ts | 9 + packages/domain/subscriptions/index.ts | 2 + packages/domain/subscriptions/schema.ts | 12 + 17 files changed, 247 insertions(+), 147 deletions(-) diff --git a/apps/bff/src/modules/invoices/invoices.controller.ts b/apps/bff/src/modules/invoices/invoices.controller.ts index 5b191dc2..0a5b6eb7 100644 --- a/apps/bff/src/modules/invoices/invoices.controller.ts +++ b/apps/bff/src/modules/invoices/invoices.controller.ts @@ -1,14 +1,4 @@ -import { - Controller, - Get, - Post, - Param, - Query, - Request, - ParseIntPipe, - HttpCode, - HttpStatus, -} from "@nestjs/common"; +import { Controller, Get, Post, Param, Query, Request, HttpCode, HttpStatus } from "@nestjs/common"; import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service.js"; import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service.js"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; @@ -17,6 +7,7 @@ import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import type { Invoice, InvoiceList, InvoiceSsoLink } from "@customer-portal/domain/billing"; import { + invoiceIdParamSchema, invoiceListQuerySchema, invoiceListSchema, invoiceSchema, @@ -37,6 +28,7 @@ import { } from "@customer-portal/domain/payments"; class InvoiceListQueryDto extends createZodDto(invoiceListQuerySchema) {} +class InvoiceIdParamDto extends createZodDto(invoiceIdParamSchema) {} class InvoiceListDto extends createZodDto(invoiceListSchema) {} class InvoiceDto extends createZodDto(invoiceSchema) {} class InvoiceSsoLinkDto extends createZodDto(invoiceSsoLinkSchema) {} @@ -104,15 +96,15 @@ export class InvoicesController { @ZodResponse({ description: "Get invoice by id", type: InvoiceDto }) async getInvoiceById( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) invoiceId: number + @Param() params: InvoiceIdParamDto ): Promise { - return this.invoicesService.getInvoiceById(req.user.id, invoiceId); + return this.invoicesService.getInvoiceById(req.user.id, params.id); } @Get(":id/subscriptions") getInvoiceSubscriptions( @Request() _req: RequestWithUser, - @Param("id", ParseIntPipe) _invoiceId: number + @Param() _params: InvoiceIdParamDto ): Subscription[] { // This functionality has been moved to WHMCS directly // For now, return empty array as subscriptions are managed in WHMCS @@ -124,7 +116,7 @@ export class InvoicesController { @ZodResponse({ description: "Create invoice SSO link", type: InvoiceSsoLinkDto }) async createSsoLink( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) invoiceId: number, + @Param() params: InvoiceIdParamDto, @Query() query: InvoiceSsoQueryDto ): Promise { const mapping = await this.mappingsService.findByUserId(req.user.id); @@ -136,7 +128,7 @@ export class InvoicesController { const ssoUrl = await this.whmcsService.whmcsSsoForInvoice( mapping.whmcsClientId, - invoiceId, + params.id, parsedQuery.target ); @@ -151,7 +143,7 @@ export class InvoicesController { @ZodResponse({ description: "Create invoice payment link", type: InvoicePaymentLinkDto }) async createPaymentLink( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) invoiceId: number, + @Param() params: InvoiceIdParamDto, @Query() query: InvoicePaymentLinkQueryDto ): Promise { const mapping = await this.mappingsService.findByUserId(req.user.id); @@ -163,7 +155,7 @@ export class InvoicesController { const ssoResult = await this.whmcsService.createPaymentSsoToken( mapping.whmcsClientId, - invoiceId, + params.id, parsedQuery.paymentMethodId, parsedQuery.gatewayName ); diff --git a/apps/bff/src/modules/notifications/notifications.controller.ts b/apps/bff/src/modules/notifications/notifications.controller.ts index 4478ac26..93779ce1 100644 --- a/apps/bff/src/modules/notifications/notifications.controller.ts +++ b/apps/bff/src/modules/notifications/notifications.controller.ts @@ -11,6 +11,7 @@ import { NotificationService } from "./notifications.service.js"; import { notificationListResponseSchema, notificationUnreadCountResponseSchema, + notificationIdParamSchema, type NotificationListResponse, } from "@customer-portal/domain/notifications"; import { notificationQuerySchema } from "@customer-portal/domain/notifications"; @@ -21,6 +22,7 @@ import { import { createZodDto, ZodResponse } from "nestjs-zod"; class NotificationQueryDto extends createZodDto(notificationQuerySchema) {} +class NotificationIdParamDto extends createZodDto(notificationIdParamSchema) {} class NotificationListResponseDto extends createZodDto(notificationListResponseSchema) {} class NotificationUnreadCountResponseDto extends createZodDto( notificationUnreadCountResponseSchema @@ -70,9 +72,9 @@ export class NotificationsController { @ZodResponse({ description: "Mark as read", type: ApiSuccessAckResponseDto }) async markAsRead( @Req() req: RequestWithUser, - @Param("id") notificationId: string + @Param() params: NotificationIdParamDto ): Promise { - await this.notificationService.markAsRead(notificationId, req.user.id); + await this.notificationService.markAsRead(params.id, req.user.id); return { success: true }; } @@ -95,9 +97,9 @@ export class NotificationsController { @ZodResponse({ description: "Dismiss notification", type: ApiSuccessAckResponseDto }) async dismiss( @Req() req: RequestWithUser, - @Param("id") notificationId: string + @Param() params: NotificationIdParamDto ): Promise { - await this.notificationService.dismiss(notificationId, req.user.id); + await this.notificationService.dismiss(params.id, req.user.id); return { success: true }; } } diff --git a/apps/bff/src/modules/orders/controllers/checkout.controller.ts b/apps/bff/src/modules/orders/controllers/checkout.controller.ts index 269994d4..0e5c3102 100644 --- a/apps/bff/src/modules/orders/controllers/checkout.controller.ts +++ b/apps/bff/src/modules/orders/controllers/checkout.controller.ts @@ -7,10 +7,9 @@ import { CheckoutSessionService } from "../services/checkout-session.service.js" import { checkoutCartSchema, checkoutBuildCartRequestSchema, - checkoutBuildCartResponseSchema, checkoutSessionIdParamSchema, - checkoutSessionResponseSchema, - checkoutValidateCartResponseSchema, + checkoutSessionDataSchema, + checkoutValidateCartDataSchema, } from "@customer-portal/domain/orders"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.js"; @@ -18,9 +17,9 @@ import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards class CheckoutBuildCartRequestDto extends createZodDto(checkoutBuildCartRequestSchema) {} class CheckoutSessionIdParamDto extends createZodDto(checkoutSessionIdParamSchema) {} class CheckoutCartDto extends createZodDto(checkoutCartSchema) {} -class CheckoutBuildCartResponseDto extends createZodDto(checkoutBuildCartResponseSchema) {} -class CheckoutSessionResponseDto extends createZodDto(checkoutSessionResponseSchema) {} -class ValidateCartResponseDto extends createZodDto(checkoutValidateCartResponseSchema) {} +class CheckoutBuildCartResponseDto extends createZodDto(checkoutCartSchema) {} +class CheckoutSessionResponseDto extends createZodDto(checkoutSessionDataSchema) {} +class ValidateCartResponseDto extends createZodDto(checkoutValidateCartDataSchema) {} @Controller("checkout") @Public() // Cart building and validation can be done without authentication @@ -52,7 +51,7 @@ export class CheckoutController { req.user?.id ); - return { success: true as const, data: cart }; + return cart; } catch (error) { this.logger.error("Failed to build checkout cart", { error: error instanceof Error ? error.message : String(error), @@ -90,15 +89,12 @@ export class CheckoutController { const session = await this.checkoutSessions.createSession(body, cart); return { - success: true as const, - data: { - sessionId: session.sessionId, - expiresAt: session.expiresAt, - orderType: body.orderType, - cart: { - items: cart.items, - totals: cart.totals, - }, + sessionId: session.sessionId, + expiresAt: session.expiresAt, + orderType: body.orderType, + cart: { + items: cart.items, + totals: cart.totals, }, }; } @@ -113,15 +109,12 @@ export class CheckoutController { async getSession(@Param() params: CheckoutSessionIdParamDto) { const session = await this.checkoutSessions.getSession(params.sessionId); return { - success: true as const, - data: { - sessionId: params.sessionId, - expiresAt: session.expiresAt, - orderType: session.request.orderType, - cart: { - items: session.cart.items, - totals: session.cart.totals, - }, + sessionId: params.sessionId, + expiresAt: session.expiresAt, + orderType: session.request.orderType, + cart: { + items: session.cart.items, + totals: session.cart.totals, }, }; } @@ -140,7 +133,7 @@ export class CheckoutController { try { this.checkoutService.validateCart(cart); - return { success: true as const, data: { valid: true } }; + return { valid: true }; } catch (error) { this.logger.error("Checkout cart validation failed", { error: error instanceof Error ? error.message : String(error), diff --git a/apps/bff/src/modules/orders/orders.controller.ts b/apps/bff/src/modules/orders/orders.controller.ts index 16105bbe..5316154a 100644 --- a/apps/bff/src/modules/orders/orders.controller.ts +++ b/apps/bff/src/modules/orders/orders.controller.ts @@ -25,7 +25,6 @@ import { orderListResponseSchema, type CreateOrderRequest, } 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"; @@ -37,9 +36,7 @@ import { SkipSuccessEnvelope } from "@bff/core/http/transform.interceptor.js"; class CreateOrderRequestDto extends createZodDto(createOrderRequestSchema) {} class CheckoutSessionCreateOrderDto extends createZodDto(checkoutSessionCreateOrderRequestSchema) {} class SfOrderIdParamDto extends createZodDto(sfOrderIdParamSchema) {} -class CreateOrderResponseDto extends createZodDto( - apiSuccessResponseSchema(orderCreateResponseSchema) -) {} +class CreateOrderResponseDto extends createZodDto(orderCreateResponseSchema) {} class OrderDetailsDto extends createZodDto(orderDetailsSchema) {} class OrderListResponseDto extends createZodDto(orderListResponseSchema) {} @@ -70,7 +67,7 @@ export class OrdersController { try { const result = await this.orderOrchestrator.createOrder(req.user.id, body); - return { success: true as const, data: result }; + return result; } catch (error) { this.logger.error( { @@ -137,7 +134,7 @@ export class OrdersController { await this.checkoutSessions.deleteSession(body.checkoutSessionId); - return { success: true as const, data: result }; + return result; } @Get("user") diff --git a/apps/bff/src/modules/services/internet-eligibility.controller.ts b/apps/bff/src/modules/services/internet-eligibility.controller.ts index 6ed0d283..583a5f51 100644 --- a/apps/bff/src/modules/services/internet-eligibility.controller.ts +++ b/apps/bff/src/modules/services/internet-eligibility.controller.ts @@ -1,12 +1,22 @@ import { Body, Controller, Get, Header, Post, Req, UseGuards } from "@nestjs/common"; -import { createZodDto } from "nestjs-zod"; +import { createZodDto, ZodResponse } from "nestjs-zod"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js"; import { InternetServicesService } from "./services/internet-services.service.js"; import type { InternetEligibilityDetails } from "@customer-portal/domain/services"; -import { internetEligibilityRequestSchema } from "@customer-portal/domain/services"; +import { + internetEligibilityDetailsSchema, + internetEligibilityRequestSchema, + internetEligibilityRequestResponseSchema, +} from "@customer-portal/domain/services"; class EligibilityRequestDto extends createZodDto(internetEligibilityRequestSchema) {} +class InternetEligibilityDetailsResponseDto extends createZodDto( + internetEligibilityDetailsSchema +) {} +class InternetEligibilityRequestResponseDto extends createZodDto( + internetEligibilityRequestResponseSchema +) {} /** * Internet Eligibility Controller @@ -26,6 +36,10 @@ export class InternetEligibilityController { @Get("eligibility") @RateLimit({ limit: 60, ttl: 60 }) // 60/min per IP (cheap) @Header("Cache-Control", "private, no-store") + @ZodResponse({ + description: "Get internet eligibility", + type: InternetEligibilityDetailsResponseDto, + }) async getEligibility(@Req() req: RequestWithUser): Promise { return this.internetCatalog.getEligibilityDetailsForUser(req.user.id); } @@ -33,6 +47,10 @@ export class InternetEligibilityController { @Post("eligibility-request") @RateLimit({ limit: 5, ttl: 300 }) // 5 per 5 minutes per IP @Header("Cache-Control", "private, no-store") + @ZodResponse({ + description: "Request internet eligibility check", + type: InternetEligibilityRequestResponseDto, + }) async requestEligibility( @Req() req: RequestWithUser, @Body() body: EligibilityRequestDto diff --git a/apps/bff/src/modules/subscriptions/subscriptions.controller.ts b/apps/bff/src/modules/subscriptions/subscriptions.controller.ts index 6870a6ff..24fee272 100644 --- a/apps/bff/src/modules/subscriptions/subscriptions.controller.ts +++ b/apps/bff/src/modules/subscriptions/subscriptions.controller.ts @@ -6,7 +6,6 @@ import { Query, Body, Request, - ParseIntPipe, Header, UseGuards, } from "@nestjs/common"; @@ -18,6 +17,8 @@ import { SimTopUpPricingService } from "./sim-management/services/sim-topup-pric import { subscriptionQuerySchema, subscriptionListSchema, + subscriptionArraySchema, + subscriptionIdParamSchema, subscriptionSchema, subscriptionStatsSchema, simActionResponseSchema, @@ -32,8 +33,6 @@ import type { SimPlanChangeResult, } from "@customer-portal/domain/subscriptions"; import type { InvoiceList } from "@customer-portal/domain/billing"; -import type { ApiSuccessResponse } from "@customer-portal/domain/common"; -import { apiSuccessResponseSchema } from "@customer-portal/domain/common"; import { createPaginationSchema } from "@customer-portal/domain/toolkit/validation/helpers"; import { simTopupRequestSchema, @@ -47,12 +46,22 @@ import { simHistoryQuerySchema, simSftpListQuerySchema, simCallHistoryImportQuerySchema, + simHistoryAvailableMonthsSchema, + simSftpListResultSchema, + simCallHistoryImportResultSchema, + simTopUpPricingSchema, simTopUpPricingPreviewRequestSchema, + simTopUpPricingPreviewResponseSchema, simReissueEsimRequestSchema, simInfoSchema, simDetailsSchema, simUsageSchema, simTopUpHistorySchema, + simAvailablePlanArraySchema, + simCancellationPreviewSchema, + simDomesticCallHistoryResponseSchema, + simInternationalCallHistoryResponseSchema, + simSmsHistoryResponseSchema, type SimAvailablePlan, type SimCancellationPreview, type SimDomesticCallHistoryResponse, @@ -81,6 +90,7 @@ const subscriptionInvoiceQuerySchema = createPaginationSchema({ class SubscriptionQueryDto extends createZodDto(subscriptionQuerySchema) {} class SubscriptionInvoiceQueryDto extends createZodDto(subscriptionInvoiceQuerySchema) {} +class SubscriptionIdParamDto extends createZodDto(subscriptionIdParamSchema) {} class SimTopupRequestDto extends createZodDto(simTopupRequestSchema) {} class SimChangePlanRequestDto extends createZodDto(simChangePlanRequestSchema) {} class SimCancelRequestDto extends createZodDto(simCancelRequestSchema) {} @@ -103,15 +113,35 @@ class SimUsageDto extends createZodDto(simUsageSchema) {} class SimTopUpHistoryDto extends createZodDto(simTopUpHistorySchema) {} class SubscriptionListDto extends createZodDto(subscriptionListSchema) {} +class ActiveSubscriptionsDto extends createZodDto(subscriptionArraySchema) {} class SubscriptionDto extends createZodDto(subscriptionSchema) {} class SubscriptionStatsDto extends createZodDto(subscriptionStatsSchema) {} class SimActionResponseDto extends createZodDto(simActionResponseSchema) {} class SimPlanChangeResultDto extends createZodDto(simPlanChangeResultSchema) {} class InvoiceListDto extends createZodDto(invoiceListSchema) {} class InternetCancellationPreviewResponseDto extends createZodDto( - apiSuccessResponseSchema(internetCancellationPreviewSchema) + internetCancellationPreviewSchema ) {} +class SimHistoryAvailableMonthsResponseDto extends createZodDto(simHistoryAvailableMonthsSchema) {} +class SimSftpListResultResponseDto extends createZodDto(simSftpListResultSchema) {} +class SimCallHistoryImportResultResponseDto extends createZodDto( + simCallHistoryImportResultSchema +) {} +class SimTopUpPricingResponseDto extends createZodDto(simTopUpPricingSchema) {} +class SimTopUpPricingPreviewResponseDto extends createZodDto( + simTopUpPricingPreviewResponseSchema +) {} +class SimAvailablePlansResponseDto extends createZodDto(simAvailablePlanArraySchema) {} +class SimCancellationPreviewResponseDto extends createZodDto(simCancellationPreviewSchema) {} +class SimDomesticCallHistoryResponseDto extends createZodDto( + simDomesticCallHistoryResponseSchema +) {} +class SimInternationalCallHistoryResponseDto extends createZodDto( + simInternationalCallHistoryResponseSchema +) {} +class SimSmsHistoryResponseDto extends createZodDto(simSmsHistoryResponseSchema) {} + @Controller("subscriptions") export class SubscriptionsController { constructor( @@ -138,7 +168,7 @@ export class SubscriptionsController { @Get("active") @Header("Cache-Control", "private, max-age=300") // 5 minutes, user-specific - @ZodResponse({ description: "List active subscriptions", type: [SubscriptionDto] }) + @ZodResponse({ description: "List active subscriptions", type: ActiveSubscriptionsDto }) async getActiveSubscriptions(@Request() req: RequestWithUser): Promise { return this.subscriptionsService.getActiveSubscriptions(req.user.id); } @@ -158,9 +188,13 @@ export class SubscriptionsController { @Public() @Get("sim/call-history/available-months") @Header("Cache-Control", "public, max-age=3600") + @ZodResponse({ + description: "Get available call/SMS history months", + type: SimHistoryAvailableMonthsResponseDto, + }) async getAvailableMonths() { const months = await this.simCallHistoryService.getAvailableMonths(); - return { success: true, data: months }; + return months; } /** @@ -168,10 +202,11 @@ export class SubscriptionsController { */ @UseGuards(AdminGuard) @Get("sim/call-history/sftp-files") + @ZodResponse({ description: "List available SFTP files", type: SimSftpListResultResponseDto }) async listSftpFiles(@Query() query: SimSftpListQueryDto) { const parsedQuery = simSftpListQuerySchema.parse(query as unknown); const files = await this.simCallHistoryService.listSftpFiles(parsedQuery.path); - return { success: true, data: files, path: parsedQuery.path }; + return { files, path: parsedQuery.path }; } /** @@ -179,28 +214,33 @@ export class SubscriptionsController { */ @UseGuards(AdminGuard) @Post("sim/call-history/import") + @ZodResponse({ + description: "Import call history (admin)", + type: SimCallHistoryImportResultResponseDto, + }) async importCallHistory(@Query() query: SimCallHistoryImportQueryDto) { const parsedQuery = simCallHistoryImportQuerySchema.parse(query as unknown); const result = await this.simCallHistoryService.importCallHistory(parsedQuery.month); - return { - success: true, - message: `Imported ${result.domestic} domestic calls, ${result.international} international calls, ${result.sms} SMS`, - data: result, - }; + return result; } @Get("sim/top-up/pricing") @Header("Cache-Control", "public, max-age=3600") // 1 hour, pricing is relatively static + @ZodResponse({ description: "Get SIM top-up pricing", type: SimTopUpPricingResponseDto }) async getSimTopUpPricing() { const pricing = await this.simTopUpPricingService.getTopUpPricing(); - return { success: true, data: pricing }; + return pricing; } @Get("sim/top-up/pricing/preview") @Header("Cache-Control", "public, max-age=3600") // 1 hour, pricing calculation is deterministic + @ZodResponse({ + description: "Preview SIM top-up pricing", + type: SimTopUpPricingPreviewResponseDto, + }) async previewSimTopUpPricing(@Query() query: SimTopUpPricingPreviewRequestDto) { const preview = await this.simTopUpPricingService.calculatePricingPreview(query.quotaMb); - return { success: true, data: preview }; + return preview; } @Get("debug/sim-details/:account") @@ -216,9 +256,9 @@ export class SubscriptionsController { @ZodResponse({ description: "Get subscription", type: SubscriptionDto }) async getSubscriptionById( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number + @Param() params: SubscriptionIdParamDto ): Promise { - return this.subscriptionsService.getSubscriptionById(req.user.id, subscriptionId); + return this.subscriptionsService.getSubscriptionById(req.user.id, params.id); } @Get(":id/invoices") @@ -226,10 +266,10 @@ export class SubscriptionsController { @ZodResponse({ description: "Get subscription invoices", type: InvoiceListDto }) async getSubscriptionInvoices( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Query() query: SubscriptionInvoiceQueryDto ): Promise { - return this.subscriptionsService.getSubscriptionInvoices(req.user.id, subscriptionId, query); + return this.subscriptionsService.getSubscriptionInvoices(req.user.id, params.id, query); } // ==================== SIM Management Endpoints (subscription-specific) ==================== @@ -238,56 +278,47 @@ export class SubscriptionsController { @UseGuards(AdminGuard) async debugSimSubscription( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number + @Param() params: SubscriptionIdParamDto ): Promise> { - return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId); + return this.simManagementService.debugSimSubscription(req.user.id, params.id); } @Get(":id/sim") @ZodResponse({ description: "Get SIM info", type: SimInfoDto }) - async getSimInfo( - @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number - ) { - return this.simManagementService.getSimInfo(req.user.id, subscriptionId); + async getSimInfo(@Request() req: RequestWithUser, @Param() params: SubscriptionIdParamDto) { + return this.simManagementService.getSimInfo(req.user.id, params.id); } @Get(":id/sim/details") @ZodResponse({ description: "Get SIM details", type: SimDetailsDto }) - async getSimDetails( - @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number - ) { - return this.simManagementService.getSimDetails(req.user.id, subscriptionId); + async getSimDetails(@Request() req: RequestWithUser, @Param() params: SubscriptionIdParamDto) { + return this.simManagementService.getSimDetails(req.user.id, params.id); } @Get(":id/sim/usage") @ZodResponse({ description: "Get SIM usage", type: SimUsageDto }) - async getSimUsage( - @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number - ) { - return this.simManagementService.getSimUsage(req.user.id, subscriptionId); + async getSimUsage(@Request() req: RequestWithUser, @Param() params: SubscriptionIdParamDto) { + return this.simManagementService.getSimUsage(req.user.id, params.id); } @Get(":id/sim/top-up-history") @ZodResponse({ description: "Get SIM top-up history", type: SimTopUpHistoryDto }) async getSimTopUpHistory( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Query() query: SimTopUpHistoryRequestDto ) { - return this.simManagementService.getSimTopUpHistory(req.user.id, subscriptionId, query); + return this.simManagementService.getSimTopUpHistory(req.user.id, params.id, query); } @Post(":id/sim/top-up") @ZodResponse({ description: "Top up SIM", type: SimActionResponseDto }) async topUpSim( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimTopupRequestDto ): Promise { - await this.simManagementService.topUpSim(req.user.id, subscriptionId, body); + await this.simManagementService.topUpSim(req.user.id, params.id, body); return { success: true, message: "SIM top-up completed successfully" }; } @@ -295,10 +326,10 @@ export class SubscriptionsController { @ZodResponse({ description: "Change SIM plan", type: SimPlanChangeResultDto }) async changeSimPlan( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimChangePlanRequestDto ): Promise { - const result = await this.simManagementService.changeSimPlan(req.user.id, subscriptionId, body); + const result = await this.simManagementService.changeSimPlan(req.user.id, params.id, body); return { success: true, message: "SIM plan change completed successfully", @@ -310,25 +341,22 @@ export class SubscriptionsController { @ZodResponse({ description: "Cancel SIM", type: SimActionResponseDto }) async cancelSim( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimCancelRequestDto ): Promise { - await this.simManagementService.cancelSim(req.user.id, subscriptionId, body); + await this.simManagementService.cancelSim(req.user.id, params.id, body); return { success: true, message: "SIM cancellation completed successfully" }; } @Post(":id/sim/reissue-esim") + @ZodResponse({ description: "Reissue eSIM profile", type: SimActionResponseDto }) async reissueEsimProfile( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimReissueEsimRequestDto ): Promise { const parsedBody = simReissueEsimRequestSchema.parse(body as unknown); - await this.simManagementService.reissueEsimProfile( - req.user.id, - subscriptionId, - parsedBody.newEid - ); + await this.simManagementService.reissueEsimProfile(req.user.id, params.id, parsedBody.newEid); return { success: true, message: "eSIM profile reissue completed successfully" }; } @@ -336,10 +364,10 @@ export class SubscriptionsController { @ZodResponse({ description: "Update SIM features", type: SimActionResponseDto }) async updateSimFeatures( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimFeaturesRequestDto ): Promise { - await this.simManagementService.updateSimFeatures(req.user.id, subscriptionId, body); + await this.simManagementService.updateSimFeatures(req.user.id, params.id, body); return { success: true, message: "SIM features updated successfully" }; } @@ -350,12 +378,13 @@ export class SubscriptionsController { */ @Get(":id/sim/available-plans") @Header("Cache-Control", "private, max-age=300") + @ZodResponse({ description: "Get available SIM plans", type: SimAvailablePlansResponseDto }) async getAvailablePlans( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number - ): Promise> { - const plans = await this.simPlanService.getAvailablePlans(req.user.id, subscriptionId); - return { success: true, data: plans }; + @Param() params: SubscriptionIdParamDto + ): Promise { + const plans = await this.simPlanService.getAvailablePlans(req.user.id, params.id); + return plans; } /** @@ -365,10 +394,10 @@ export class SubscriptionsController { @ZodResponse({ description: "Change SIM plan (full)", type: SimPlanChangeResultDto }) async changeSimPlanFull( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimChangePlanFullRequestDto ): Promise { - const result = await this.simPlanService.changeSimPlanFull(req.user.id, subscriptionId, body); + const result = await this.simPlanService.changeSimPlanFull(req.user.id, params.id, body); return { success: true, message: `SIM plan change scheduled for ${result.scheduledAt}`, @@ -381,15 +410,19 @@ export class SubscriptionsController { */ @Get(":id/sim/cancellation-preview") @Header("Cache-Control", "private, max-age=60") + @ZodResponse({ + description: "Get SIM cancellation preview", + type: SimCancellationPreviewResponseDto, + }) async getCancellationPreview( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number - ): Promise> { + @Param() params: SubscriptionIdParamDto + ): Promise { const preview = await this.simCancellationService.getCancellationPreview( req.user.id, - subscriptionId + params.id ); - return { success: true, data: preview }; + return preview; } /** @@ -399,10 +432,10 @@ export class SubscriptionsController { @ZodResponse({ description: "Cancel SIM (full)", type: SimActionResponseDto }) async cancelSimFull( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimCancelFullRequestDto ): Promise { - await this.simCancellationService.cancelSimFull(req.user.id, subscriptionId, body); + await this.simCancellationService.cancelSimFull(req.user.id, params.id, body); return { success: true, message: `SIM cancellation scheduled for end of ${body.cancellationMonth}`, @@ -416,10 +449,10 @@ export class SubscriptionsController { @ZodResponse({ description: "Reissue SIM", type: SimActionResponseDto }) async reissueSim( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: SimReissueFullRequestDto ): Promise { - await this.esimManagementService.reissueSim(req.user.id, subscriptionId, body); + await this.esimManagementService.reissueSim(req.user.id, params.id, body); if (body.simType === "esim") { return { success: true, message: "eSIM profile reissue request submitted" }; @@ -444,13 +477,13 @@ export class SubscriptionsController { }) async getInternetCancellationPreview( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number + @Param() params: SubscriptionIdParamDto ) { const preview = await this.internetCancellationService.getCancellationPreview( req.user.id, - subscriptionId + params.id ); - return { success: true as const, data: preview }; + return preview; } /** @@ -460,10 +493,10 @@ export class SubscriptionsController { @ZodResponse({ description: "Cancel internet", type: SimActionResponseDto }) async cancelInternet( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Body() body: InternetCancelRequestDto ): Promise { - await this.internetCancellationService.submitCancellation(req.user.id, subscriptionId, body); + await this.internetCancellationService.submitCancellation(req.user.id, params.id, body); return { success: true, message: `Internet cancellation scheduled for end of ${body.cancellationMonth}`, @@ -477,20 +510,24 @@ export class SubscriptionsController { */ @Get(":id/sim/call-history/domestic") @Header("Cache-Control", "private, max-age=300") + @ZodResponse({ + description: "Get domestic call history", + type: SimDomesticCallHistoryResponseDto, + }) async getDomesticCallHistory( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Query() query: SimHistoryQueryDto - ): Promise> { + ): Promise { const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const result = await this.simCallHistoryService.getDomesticCallHistory( req.user.id, - subscriptionId, + params.id, parsedQuery.month, parsedQuery.page, parsedQuery.limit ); - return { success: true, data: result }; + return result; } /** @@ -498,20 +535,24 @@ export class SubscriptionsController { */ @Get(":id/sim/call-history/international") @Header("Cache-Control", "private, max-age=300") + @ZodResponse({ + description: "Get international call history", + type: SimInternationalCallHistoryResponseDto, + }) async getInternationalCallHistory( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Query() query: SimHistoryQueryDto - ): Promise> { + ): Promise { const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const result = await this.simCallHistoryService.getInternationalCallHistory( req.user.id, - subscriptionId, + params.id, parsedQuery.month, parsedQuery.page, parsedQuery.limit ); - return { success: true, data: result }; + return result; } /** @@ -519,19 +560,20 @@ export class SubscriptionsController { */ @Get(":id/sim/sms-history") @Header("Cache-Control", "private, max-age=300") + @ZodResponse({ description: "Get SMS history", type: SimSmsHistoryResponseDto }) async getSmsHistory( @Request() req: RequestWithUser, - @Param("id", ParseIntPipe) subscriptionId: number, + @Param() params: SubscriptionIdParamDto, @Query() query: SimHistoryQueryDto - ): Promise> { + ): Promise { const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const result = await this.simCallHistoryService.getSmsHistory( req.user.id, - subscriptionId, + params.id, parsedQuery.month, parsedQuery.page, parsedQuery.limit ); - return { success: true, data: result }; + return result; } } diff --git a/packages/domain/billing/index.ts b/packages/domain/billing/index.ts index 5a28f90a..374f393e 100644 --- a/packages/domain/billing/index.ts +++ b/packages/domain/billing/index.ts @@ -18,6 +18,7 @@ export type { InvoiceStatus, InvoiceItem, Invoice, + InvoiceIdParam, InvoicePagination, InvoiceList, InvoiceSsoLink, diff --git a/packages/domain/billing/schema.ts b/packages/domain/billing/schema.ts index b3e5e03c..0d3fb55a 100644 --- a/packages/domain/billing/schema.ts +++ b/packages/domain/billing/schema.ts @@ -49,6 +49,15 @@ export const invoiceSchema = z.object({ daysOverdue: z.number().int().nonnegative().optional(), }); +// ============================================================================ +// Route Param Schemas (BFF) +// ============================================================================ + +export const invoiceIdParamSchema = z.object({ + id: z.coerce.number().int().positive("Invoice id must be positive"), +}); +export type InvoiceIdParam = z.infer; + // Invoice Pagination Schema export const invoicePaginationSchema = z.object({ page: z.number().int().nonnegative(), diff --git a/packages/domain/notifications/index.ts b/packages/domain/notifications/index.ts index 610ff795..36c9a42b 100644 --- a/packages/domain/notifications/index.ts +++ b/packages/domain/notifications/index.ts @@ -20,6 +20,7 @@ export { notificationListResponseSchema, notificationUnreadCountResponseSchema, notificationQuerySchema, + notificationIdParamSchema, // Types type Notification, type CreateNotificationRequest, @@ -27,4 +28,5 @@ export { type NotificationListResponse, type NotificationUnreadCountResponse, type NotificationQuery, + type NotificationIdParam, } from "./schema.js"; diff --git a/packages/domain/notifications/schema.ts b/packages/domain/notifications/schema.ts index 04800890..14bc783f 100644 --- a/packages/domain/notifications/schema.ts +++ b/packages/domain/notifications/schema.ts @@ -224,3 +224,12 @@ export const notificationQuerySchema = z.object({ }); export type NotificationQuery = z.infer; + +// ============================================================================= +// Route Param Schemas (BFF) +// ============================================================================= + +export const notificationIdParamSchema = z.object({ + id: z.string().uuid(), +}); +export type NotificationIdParam = z.infer; diff --git a/packages/domain/orders/schema.ts b/packages/domain/orders/schema.ts index 15c27ab2..9bf6570b 100644 --- a/packages/domain/orders/schema.ts +++ b/packages/domain/orders/schema.ts @@ -345,17 +345,18 @@ export const checkoutCartSummarySchema = z.object({ totals: checkoutTotalsSchema, }); -export const checkoutSessionResponseSchema = apiSuccessResponseSchema( - z.object({ - sessionId: z.string().uuid(), - expiresAt: z.string(), - orderType: z.enum(["Internet", "SIM", "VPN", "Other"]), - cart: checkoutCartSummarySchema, - }) -); +export const checkoutSessionDataSchema = z.object({ + sessionId: z.string().uuid(), + expiresAt: z.string(), + orderType: z.enum(["Internet", "SIM", "VPN", "Other"]), + cart: checkoutCartSummarySchema, +}); +export const checkoutSessionResponseSchema = apiSuccessResponseSchema(checkoutSessionDataSchema); + +export const checkoutValidateCartDataSchema = z.object({ valid: z.boolean() }); export const checkoutValidateCartResponseSchema = apiSuccessResponseSchema( - z.object({ valid: z.boolean() }) + checkoutValidateCartDataSchema ); /** diff --git a/packages/domain/services/index.ts b/packages/domain/services/index.ts index 3506997f..ca621e13 100644 --- a/packages/domain/services/index.ts +++ b/packages/domain/services/index.ts @@ -29,6 +29,8 @@ export type { InternetAddonCatalogItem, InternetEligibilityStatus, InternetEligibilityDetails, + InternetEligibilityRequest, + InternetEligibilityRequestResponse, // SIM products SimCatalogProduct, SimActivationFeeCatalogItem, diff --git a/packages/domain/services/schema.ts b/packages/domain/services/schema.ts index b65b2f71..1eddd7a3 100644 --- a/packages/domain/services/schema.ts +++ b/packages/domain/services/schema.ts @@ -121,6 +121,10 @@ export const internetEligibilityRequestSchema = z.object({ address: addressSchema.partial().optional(), }); +export const internetEligibilityRequestResponseSchema = z.object({ + requestId: z.string(), +}); + // ============================================================================ // SIM Product Schemas // ============================================================================ @@ -195,6 +199,9 @@ export type InternetCatalogCollection = z.infer; export type InternetEligibilityDetails = z.infer; export type InternetEligibilityRequest = z.infer; +export type InternetEligibilityRequestResponse = z.infer< + typeof internetEligibilityRequestResponseSchema +>; // SIM products export type SimCatalogProduct = z.infer; diff --git a/packages/domain/sim/index.ts b/packages/domain/sim/index.ts index 0cde1bb1..4189f6e0 100644 --- a/packages/domain/sim/index.ts +++ b/packages/domain/sim/index.ts @@ -38,6 +38,7 @@ export type { SimInfo, // Portal-facing DTOs SimAvailablePlan, + SimAvailablePlanArray, SimCancellationMonth, SimCancellationPreview, SimReissueFullRequest, @@ -52,6 +53,7 @@ export type { SimHistoryAvailableMonths, SimCallHistoryImportResult, SimSftpFiles, + SimSftpListResult, // Request types SimTopUpRequest, SimPlanChangeRequest, diff --git a/packages/domain/sim/schema.ts b/packages/domain/sim/schema.ts index de326461..6eff2e03 100644 --- a/packages/domain/sim/schema.ts +++ b/packages/domain/sim/schema.ts @@ -176,6 +176,9 @@ export const simAvailablePlanSchema = simCatalogProductSchema.extend({ export type SimAvailablePlan = z.infer; +export const simAvailablePlanArraySchema = z.array(simAvailablePlanSchema); +export type SimAvailablePlanArray = z.infer; + /** * Cancellation month option for SIM cancellation preview */ @@ -252,6 +255,12 @@ export type SimCallHistoryImportResult = z.infer; +export const simSftpListResultSchema = z.object({ + path: z.string(), + files: simSftpFilesSchema, +}); +export type SimSftpListResult = z.infer; + const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format"); const timeHmsSchema = z.string().regex(/^\d{2}:\d{2}:\d{2}$/, "Time must be in HH:MM:SS format"); diff --git a/packages/domain/subscriptions/index.ts b/packages/domain/subscriptions/index.ts index 28643ad5..cc675903 100644 --- a/packages/domain/subscriptions/index.ts +++ b/packages/domain/subscriptions/index.ts @@ -17,7 +17,9 @@ export type { SubscriptionStatus, SubscriptionCycle, Subscription, + SubscriptionArray, SubscriptionList, + SubscriptionIdParam, SubscriptionQueryParams, SubscriptionQuery, SubscriptionStats, diff --git a/packages/domain/subscriptions/schema.ts b/packages/domain/subscriptions/schema.ts index 4ec38b8d..1b642dae 100644 --- a/packages/domain/subscriptions/schema.ts +++ b/packages/domain/subscriptions/schema.ts @@ -51,12 +51,23 @@ export const subscriptionSchema = z.object({ serverName: z.string().optional(), }); +export const subscriptionArraySchema = z.array(subscriptionSchema); + // Subscription List Schema export const subscriptionListSchema = z.object({ subscriptions: z.array(subscriptionSchema), totalCount: z.number().int().nonnegative(), }); +// ============================================================================ +// Route Param Schemas (BFF) +// ============================================================================ + +export const subscriptionIdParamSchema = z.object({ + id: z.coerce.number().int().positive("Subscription id must be positive"), +}); +export type SubscriptionIdParam = z.infer; + // ============================================================================ // Query Parameter Schemas // ============================================================================ @@ -116,6 +127,7 @@ export const simPlanChangeResultSchema = apiSuccessMessageResponseSchema.extend( export type SubscriptionStatus = z.infer; export type SubscriptionCycle = z.infer; export type Subscription = z.infer; +export type SubscriptionArray = z.infer; export type SubscriptionList = z.infer; export type SubscriptionStats = z.infer; export type SimActionResponse = z.infer;