Assist_Design/apps/bff/src/modules/subscriptions/subscriptions.controller.ts

538 lines
20 KiB
TypeScript
Raw Normal View History

2025-08-22 17:02:49 +09:00
import {
Controller,
Get,
Post,
2025-08-22 17:02:49 +09:00
Param,
Query,
Body,
Request,
ParseIntPipe,
Header,
UseGuards,
2025-08-22 17:02:49 +09:00
} 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";
2025-08-28 16:57:57 +09:00
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)
) {}
2025-08-22 17:02:49 +09:00
@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<SubscriptionList> {
const { status } = query;
return this.subscriptionsService.getSubscriptions(req.user.id, { status });
}
2025-08-22 17:02:49 +09:00
@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<Subscription[]> {
2025-08-27 10:54:05 +09:00
return this.subscriptionsService.getActiveSubscriptions(req.user.id);
}
2025-08-22 17:02:49 +09:00
@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<SubscriptionStats> {
2025-08-27 10:54:05 +09:00
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<Subscription> {
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<InvoiceList> {
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<Record<string, unknown>> {
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<SimActionResponse> {
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<SimPlanChangeResult> {
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<SimActionResponse> {
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<SimActionResponse> {
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<SimActionResponse> {
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<ApiSuccessResponse<SimAvailablePlan[]>> {
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<SimPlanChangeResult> {
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<ApiSuccessResponse<SimCancellationPreview>> {
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<SimActionResponse> {
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<SimActionResponse> {
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<SubscriptionActionResponse> {
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<ApiSuccessResponse<SimDomesticCallHistoryResponse>> {
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<ApiSuccessResponse<SimInternationalCallHistoryResponse>> {
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<ApiSuccessResponse<SimSmsHistoryResponse>> {
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 };
}
}