import { Controller, Get, Post, Param, Query, Body, Request, ParseIntPipe, BadRequestException, UsePipes, Header, } from "@nestjs/common"; import { Public } from "@bff/modules/auth/decorators/public.decorator"; import { SubscriptionsService } from "./subscriptions.service"; import { SimManagementService } from "./sim-management.service"; import { SimTopUpPricingService } from "./sim-management/services/sim-topup-pricing.service"; import { Subscription, SubscriptionList, SubscriptionStats, SimActionResponse, SimPlanChangeResult, subscriptionQuerySchema, type SubscriptionQuery, } from "@customer-portal/domain/subscriptions"; import { InvoiceList } from "@customer-portal/domain/billing"; import { createPaginationSchema } from "@customer-portal/domain/toolkit/validation/helpers"; import type { z } from "zod"; import { simTopupRequestSchema, simChangePlanRequestSchema, simCancelRequestSchema, simFeaturesRequestSchema, simCancelFullRequestSchema, simChangePlanFullRequestSchema, type SimTopupRequest, type SimChangePlanRequest, type SimCancelRequest, type SimFeaturesRequest, type SimCancelFullRequest, type SimChangePlanFullRequest, } from "@customer-portal/domain/sim"; import { ZodValidationPipe } from "nestjs-zod"; import type { RequestWithUser } from "@bff/modules/auth/auth.types"; import { SimPlanService } from "./sim-management/services/sim-plan.service"; import { SimCancellationService } from "./sim-management/services/sim-cancellation.service"; import { EsimManagementService, type ReissueSimRequest } from "./sim-management/services/esim-management.service"; import { SimCallHistoryService } from "./sim-management/services/sim-call-history.service"; const subscriptionInvoiceQuerySchema = createPaginationSchema({ defaultLimit: 10, maxLimit: 100, minLimit: 1, }); type SubscriptionInvoiceQuery = z.infer; @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 ) {} @Get() @Header("Cache-Control", "private, max-age=300") // 5 minutes, user-specific @UsePipes(new ZodValidationPipe(subscriptionQuerySchema)) async getSubscriptions( @Request() req: RequestWithUser, @Query() query: SubscriptionQuery ): 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 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 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 */ @Public() @Get("sim/call-history/sftp-files") async listSftpFiles(@Query("path") path: string = "/home/PASI") { const files = await this.simCallHistoryService.listSftpFiles(path); return { success: true, data: files, path }; } /** * Trigger manual import of call history (admin only) * TODO: Add proper admin authentication before production */ @Public() @Post("sim/call-history/import") async importCallHistory(@Query("month") yearMonth: string) { if (!yearMonth || !/^\d{6}$/.test(yearMonth)) { throw new BadRequestException("Invalid month format (expected YYYYMM)"); } const result = await this.simCallHistoryService.importCallHistory(yearMonth); 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("quotaMb") quotaMb: string) { const quotaMbNum = parseInt(quotaMb, 10); if (isNaN(quotaMbNum) || quotaMbNum <= 0) { throw new BadRequestException("Invalid quotaMb parameter"); } const preview = await this.simTopUpPricingService.calculatePricingPreview(quotaMbNum); return { success: true, data: preview }; } @Get("debug/sim-details/:account") 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 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 async getSubscriptionInvoices( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Query(new ZodValidationPipe(subscriptionInvoiceQuerySchema)) query: SubscriptionInvoiceQuery ): Promise { return this.subscriptionsService.getSubscriptionInvoices(req.user.id, subscriptionId, query); } // ==================== SIM Management Endpoints (subscription-specific) ==================== @Get(":id/sim/debug") async debugSimSubscription( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ): Promise> { return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId); } @Get(":id/sim") async getSimInfo( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ) { return this.simManagementService.getSimInfo(req.user.id, subscriptionId); } @Get(":id/sim/details") async getSimDetails( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ) { return this.simManagementService.getSimDetails(req.user.id, subscriptionId); } @Get(":id/sim/usage") async getSimUsage( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number ) { return this.simManagementService.getSimUsage(req.user.id, subscriptionId); } @Get(":id/sim/top-up-history") async getSimTopUpHistory( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Query("fromDate") fromDate: string, @Query("toDate") toDate: string ) { if (!fromDate || !toDate) { throw new BadRequestException("fromDate and toDate are required"); } return this.simManagementService.getSimTopUpHistory(req.user.id, subscriptionId, { fromDate, toDate, }); } @Post(":id/sim/top-up") @UsePipes(new ZodValidationPipe(simTopupRequestSchema)) async topUpSim( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimTopupRequest ): Promise { await this.simManagementService.topUpSim(req.user.id, subscriptionId, body); return { success: true, message: "SIM top-up completed successfully" }; } @Post(":id/sim/change-plan") @UsePipes(new ZodValidationPipe(simChangePlanRequestSchema)) async changeSimPlan( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimChangePlanRequest ): 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") @UsePipes(new ZodValidationPipe(simCancelRequestSchema)) async cancelSim( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimCancelRequest ): 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: { newEid?: string } = {} ): Promise { await this.simManagementService.reissueEsimProfile(req.user.id, subscriptionId, body.newEid); return { success: true, message: "eSIM profile reissue completed successfully" }; } @Post(":id/sim/features") @UsePipes(new ZodValidationPipe(simFeaturesRequestSchema)) async updateSimFeatures( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimFeaturesRequest ): 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 ) { 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") @UsePipes(new ZodValidationPipe(simChangePlanFullRequestSchema)) async changeSimPlanFull( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimChangePlanFullRequest ): 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 ) { 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") @UsePipes(new ZodValidationPipe(simCancelFullRequestSchema)) async cancelSimFull( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: SimCancelFullRequest ): 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") async reissueSim( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number, @Body() body: ReissueSimRequest ): 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." }; } } // ==================== 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("month") month?: string, @Query("page") page?: string, @Query("limit") limit?: string ) { const pageNum = parseInt(page || "1", 10); const limitNum = parseInt(limit || "50", 10); if (isNaN(pageNum) || pageNum < 1) { throw new BadRequestException("Invalid page number"); } if (isNaN(limitNum) || limitNum < 1 || limitNum > 100) { throw new BadRequestException("Invalid limit (must be 1-100)"); } const result = await this.simCallHistoryService.getDomesticCallHistory( req.user.id, subscriptionId, month, pageNum, limitNum ); 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("month") month?: string, @Query("page") page?: string, @Query("limit") limit?: string ) { const pageNum = parseInt(page || "1", 10); const limitNum = parseInt(limit || "50", 10); if (isNaN(pageNum) || pageNum < 1) { throw new BadRequestException("Invalid page number"); } if (isNaN(limitNum) || limitNum < 1 || limitNum > 100) { throw new BadRequestException("Invalid limit (must be 1-100)"); } const result = await this.simCallHistoryService.getInternationalCallHistory( req.user.id, subscriptionId, month, pageNum, limitNum ); 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("month") month?: string, @Query("page") page?: string, @Query("limit") limit?: string ) { const pageNum = parseInt(page || "1", 10); const limitNum = parseInt(limit || "50", 10); if (isNaN(pageNum) || pageNum < 1) { throw new BadRequestException("Invalid page number"); } if (isNaN(limitNum) || limitNum < 1 || limitNum > 100) { throw new BadRequestException("Invalid limit (must be 1-100)"); } const result = await this.simCallHistoryService.getSmsHistory( req.user.id, subscriptionId, month, pageNum, limitNum ); return { success: true, data: result }; } }