import { Controller, Get, Post, Param, Query, Body, Request, ParseIntPipe, Header, UseGuards, } from "@nestjs/common"; import { Public } from "@bff/modules/auth/decorators/public.decorator.js"; import { SubscriptionsService } from "./subscriptions.service.js"; import { SimManagementService } from "./sim-management.service.js"; import { SimTopUpPricingService } from "./sim-management/services/sim-topup-pricing.service.js"; import { subscriptionQuerySchema, subscriptionListSchema, subscriptionSchema, subscriptionStatsSchema, simActionResponseSchema, simPlanChangeResultSchema, internetCancellationPreviewSchema, } from "@customer-portal/domain/subscriptions"; import type { Subscription, SubscriptionList, SubscriptionStats, SimActionResponse, 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, simChangePlanRequestSchema, simCancelRequestSchema, simFeaturesRequestSchema, simTopUpHistoryRequestSchema, simCancelFullRequestSchema, simChangePlanFullRequestSchema, simReissueFullRequestSchema, simHistoryQuerySchema, simSftpListQuerySchema, simCallHistoryImportQuerySchema, simTopUpPricingPreviewRequestSchema, simReissueEsimRequestSchema, simInfoSchema, simDetailsSchema, simUsageSchema, simTopUpHistorySchema, type SimAvailablePlan, type SimCancellationPreview, type SimDomesticCallHistoryResponse, type SimInternationalCallHistoryResponse, type SimSmsHistoryResponse, } from "@customer-portal/domain/sim"; import { createZodDto, ZodResponse } from "nestjs-zod"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { SimPlanService } from "./sim-management/services/sim-plan.service.js"; import { SimCancellationService } from "./sim-management/services/sim-cancellation.service.js"; import { AdminGuard } from "@bff/core/security/guards/admin.guard.js"; import { EsimManagementService } from "./sim-management/services/esim-management.service.js"; import { SimCallHistoryService } from "./sim-management/services/sim-call-history.service.js"; import { InternetCancellationService } from "./internet-management/services/internet-cancellation.service.js"; import { internetCancelRequestSchema, type SimActionResponse as SubscriptionActionResponse, } from "@customer-portal/domain/subscriptions"; import { invoiceListSchema } from "@customer-portal/domain/billing"; const subscriptionInvoiceQuerySchema = createPaginationSchema({ defaultLimit: 10, maxLimit: 100, minLimit: 1, }); class SubscriptionQueryDto extends createZodDto(subscriptionQuerySchema) {} class SubscriptionInvoiceQueryDto extends createZodDto(subscriptionInvoiceQuerySchema) {} class SimTopupRequestDto extends createZodDto(simTopupRequestSchema) {} class SimChangePlanRequestDto extends createZodDto(simChangePlanRequestSchema) {} class SimCancelRequestDto extends createZodDto(simCancelRequestSchema) {} class SimFeaturesRequestDto extends createZodDto(simFeaturesRequestSchema) {} class SimChangePlanFullRequestDto extends createZodDto(simChangePlanFullRequestSchema) {} class SimCancelFullRequestDto extends createZodDto(simCancelFullRequestSchema) {} class SimReissueFullRequestDto extends createZodDto(simReissueFullRequestSchema) {} class InternetCancelRequestDto extends createZodDto(internetCancelRequestSchema) {} class SimHistoryQueryDto extends createZodDto(simHistoryQuerySchema) {} class SimSftpListQueryDto extends createZodDto(simSftpListQuerySchema) {} class SimCallHistoryImportQueryDto extends createZodDto(simCallHistoryImportQuerySchema) {} class SimTopUpPricingPreviewRequestDto extends createZodDto(simTopUpPricingPreviewRequestSchema) {} class SimReissueEsimRequestDto extends createZodDto(simReissueEsimRequestSchema) {} class SimTopUpHistoryRequestDto extends createZodDto(simTopUpHistoryRequestSchema) {} class SimInfoDto extends createZodDto(simInfoSchema) {} class SimDetailsDto extends createZodDto(simDetailsSchema) {} class SimUsageDto extends createZodDto(simUsageSchema) {} class SimTopUpHistoryDto extends createZodDto(simTopUpHistorySchema) {} class SubscriptionListDto extends createZodDto(subscriptionListSchema) {} 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) ) {} @Controller("subscriptions") export class SubscriptionsController { constructor( private readonly subscriptionsService: SubscriptionsService, private readonly simManagementService: SimManagementService, private readonly simTopUpPricingService: SimTopUpPricingService, private readonly simPlanService: SimPlanService, private readonly simCancellationService: SimCancellationService, private readonly esimManagementService: EsimManagementService, private readonly simCallHistoryService: SimCallHistoryService, private readonly internetCancellationService: InternetCancellationService ) {} @Get() @Header("Cache-Control", "private, max-age=300") // 5 minutes, user-specific @ZodResponse({ description: "List subscriptions", type: SubscriptionListDto }) async getSubscriptions( @Request() req: RequestWithUser, @Query() query: SubscriptionQueryDto ): Promise { const { status } = query; return this.subscriptionsService.getSubscriptions(req.user.id, { status }); } @Get("active") @Header("Cache-Control", "private, max-age=300") // 5 minutes, user-specific @ZodResponse({ description: "List active subscriptions", type: [SubscriptionDto] }) async getActiveSubscriptions(@Request() req: RequestWithUser): Promise { return this.subscriptionsService.getActiveSubscriptions(req.user.id); } @Get("stats") @Header("Cache-Control", "private, max-age=300") // 5 minutes, user-specific @ZodResponse({ description: "Get subscription stats", type: SubscriptionStatsDto }) async getSubscriptionStats(@Request() req: RequestWithUser): Promise { return this.subscriptionsService.getSubscriptionStats(req.user.id); } // ==================== Static SIM Routes (must be before :id routes) ==================== /** * Get available months for call/SMS history */ @Public() @Get("sim/call-history/available-months") @Header("Cache-Control", "public, max-age=3600") async getAvailableMonths() { const months = await this.simCallHistoryService.getAvailableMonths(); return { success: true, data: months }; } /** * List available files on SFTP for debugging */ @UseGuards(AdminGuard) @Get("sim/call-history/sftp-files") 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 }; } /** * Trigger manual import of call history (admin only) */ @UseGuards(AdminGuard) @Post("sim/call-history/import") 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, }; } @Get("sim/top-up/pricing") @Header("Cache-Control", "public, max-age=3600") // 1 hour, pricing is relatively static async getSimTopUpPricing() { const pricing = await this.simTopUpPricingService.getTopUpPricing(); return { success: true, data: pricing }; } @Get("sim/top-up/pricing/preview") @Header("Cache-Control", "public, max-age=3600") // 1 hour, pricing calculation is deterministic async previewSimTopUpPricing(@Query() query: SimTopUpPricingPreviewRequestDto) { const preview = await this.simTopUpPricingService.calculatePricingPreview(query.quotaMb); return { success: true, data: preview }; } @Get("debug/sim-details/:account") @UseGuards(AdminGuard) async debugSimDetails(@Param("account") account: string) { return await this.simManagementService.getSimDetailsDebug(account); } // ==================== Dynamic :id Routes ==================== @Get(":id") @Header("Cache-Control", "private, max-age=300") // 5 minutes, user-specific @ZodResponse({ description: "Get subscription", type: SubscriptionDto }) async getSubscriptionById( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ): Promise { return this.subscriptionsService.getSubscriptionById(req.user.id, subscriptionId); } @Get(":id/invoices") @Header("Cache-Control", "private, max-age=60") // 1 minute, may update with payments @ZodResponse({ description: "Get subscription invoices", type: InvoiceListDto }) async getSubscriptionInvoices( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Query() query: SubscriptionInvoiceQueryDto ): Promise { return this.subscriptionsService.getSubscriptionInvoices(req.user.id, subscriptionId, query); } // ==================== SIM Management Endpoints (subscription-specific) ==================== @Get(":id/sim/debug") @UseGuards(AdminGuard) async debugSimSubscription( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ): Promise> { return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId); } @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); } @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); } @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); } @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, @Query() query: SimTopUpHistoryRequestDto ) { return this.simManagementService.getSimTopUpHistory(req.user.id, subscriptionId, query); } @Post(":id/sim/top-up") @ZodResponse({ description: "Top up SIM", type: SimActionResponseDto }) async topUpSim( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimTopupRequestDto ): Promise { await this.simManagementService.topUpSim(req.user.id, subscriptionId, body); return { success: true, message: "SIM top-up completed successfully" }; } @Post(":id/sim/change-plan") @ZodResponse({ description: "Change SIM plan", type: SimPlanChangeResultDto }) async changeSimPlan( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimChangePlanRequestDto ): Promise { const result = await this.simManagementService.changeSimPlan(req.user.id, subscriptionId, body); return { success: true, message: "SIM plan change completed successfully", ...result, }; } @Post(":id/sim/cancel") @ZodResponse({ description: "Cancel SIM", type: SimActionResponseDto }) async cancelSim( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimCancelRequestDto ): Promise { await this.simManagementService.cancelSim(req.user.id, subscriptionId, body); return { success: true, message: "SIM cancellation completed successfully" }; } @Post(":id/sim/reissue-esim") async reissueEsimProfile( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimReissueEsimRequestDto ): Promise { const parsedBody = simReissueEsimRequestSchema.parse(body as unknown); await this.simManagementService.reissueEsimProfile( req.user.id, subscriptionId, parsedBody.newEid ); return { success: true, message: "eSIM profile reissue completed successfully" }; } @Post(":id/sim/features") @ZodResponse({ description: "Update SIM features", type: SimActionResponseDto }) async updateSimFeatures( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimFeaturesRequestDto ): Promise { await this.simManagementService.updateSimFeatures(req.user.id, subscriptionId, body); return { success: true, message: "SIM features updated successfully" }; } // ==================== Enhanced SIM Management Endpoints ==================== /** * Get available plans for plan change (filtered by current plan type) */ @Get(":id/sim/available-plans") @Header("Cache-Control", "private, max-age=300") 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 }; } /** * Change SIM plan with enhanced flow (Salesforce SKU mapping + email notifications) */ @Post(":id/sim/change-plan-full") @ZodResponse({ description: "Change SIM plan (full)", type: SimPlanChangeResultDto }) async changeSimPlanFull( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimChangePlanFullRequestDto ): Promise { const result = await this.simPlanService.changeSimPlanFull(req.user.id, subscriptionId, body); return { success: true, message: `SIM plan change scheduled for ${result.scheduledAt}`, ...result, }; } /** * Get cancellation preview (available months, customer info, minimum contract term) */ @Get(":id/sim/cancellation-preview") @Header("Cache-Control", "private, max-age=60") async getCancellationPreview( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ): Promise> { const preview = await this.simCancellationService.getCancellationPreview( req.user.id, subscriptionId ); return { success: true, data: preview }; } /** * Cancel SIM with full flow (PA02-04 + email notifications) */ @Post(":id/sim/cancel-full") @ZodResponse({ description: "Cancel SIM (full)", type: SimActionResponseDto }) async cancelSimFull( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimCancelFullRequestDto ): Promise { await this.simCancellationService.cancelSimFull(req.user.id, subscriptionId, body); return { success: true, message: `SIM cancellation scheduled for end of ${body.cancellationMonth}`, }; } /** * Reissue SIM (both eSIM and physical SIM) */ @Post(":id/sim/reissue") @ZodResponse({ description: "Reissue SIM", type: SimActionResponseDto }) async reissueSim( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimReissueFullRequestDto ): Promise { await this.esimManagementService.reissueSim(req.user.id, subscriptionId, body); if (body.simType === "esim") { return { success: true, message: "eSIM profile reissue request submitted" }; } else { return { success: true, message: "Physical SIM reissue request submitted. You will be contacted shortly.", }; } } // ==================== Internet Management Endpoints ==================== /** * Get Internet cancellation preview (available months, service details) */ @Get(":id/internet/cancellation-preview") @Header("Cache-Control", "private, max-age=60") @ZodResponse({ description: "Get internet cancellation preview", type: InternetCancellationPreviewResponseDto, }) async getInternetCancellationPreview( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ) { const preview = await this.internetCancellationService.getCancellationPreview( req.user.id, subscriptionId ); return { success: true as const, data: preview }; } /** * Submit Internet cancellation request */ @Post(":id/internet/cancel") @ZodResponse({ description: "Cancel internet", type: SimActionResponseDto }) async cancelInternet( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: InternetCancelRequestDto ): Promise { await this.internetCancellationService.submitCancellation(req.user.id, subscriptionId, body); return { success: true, message: `Internet cancellation scheduled for end of ${body.cancellationMonth}`, }; } // ==================== Call/SMS History Endpoints ==================== /** * Get domestic call history */ @Get(":id/sim/call-history/domestic") @Header("Cache-Control", "private, max-age=300") async getDomesticCallHistory( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Query() query: SimHistoryQueryDto ): Promise> { const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const result = await this.simCallHistoryService.getDomesticCallHistory( req.user.id, subscriptionId, parsedQuery.month, parsedQuery.page, parsedQuery.limit ); return { success: true, data: result }; } /** * Get international call history */ @Get(":id/sim/call-history/international") @Header("Cache-Control", "private, max-age=300") async getInternationalCallHistory( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Query() query: SimHistoryQueryDto ): Promise> { const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const result = await this.simCallHistoryService.getInternationalCallHistory( req.user.id, subscriptionId, parsedQuery.month, parsedQuery.page, parsedQuery.limit ); return { success: true, data: result }; } /** * Get SMS history */ @Get(":id/sim/sms-history") @Header("Cache-Control", "private, max-age=300") async getSmsHistory( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Query() query: SimHistoryQueryDto ): Promise> { const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const result = await this.simCallHistoryService.getSmsHistory( req.user.id, subscriptionId, parsedQuery.month, parsedQuery.page, parsedQuery.limit ); return { success: true, data: result }; } }