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

420 lines
13 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,
BadRequestException,
2025-08-22 17:02:49 +09:00
} from "@nestjs/common";
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiQuery,
ApiBearerAuth,
ApiParam,
ApiBody,
2025-08-22 17:02:49 +09:00
} from "@nestjs/swagger";
import { SubscriptionsService } from "./subscriptions.service";
import { SimManagementService } from "./sim-management.service";
2025-08-28 16:57:57 +09:00
2025-08-22 17:02:49 +09:00
import { Subscription, SubscriptionList, InvoiceList } from "@customer-portal/shared";
import type { RequestWithUser } from "../auth/auth.types";
2025-08-22 17:02:49 +09:00
@ApiTags("subscriptions")
@Controller("subscriptions")
@ApiBearerAuth()
export class SubscriptionsController {
constructor(
private readonly subscriptionsService: SubscriptionsService,
private readonly simManagementService: SimManagementService,
) {}
@Get()
2025-08-22 17:02:49 +09:00
@ApiOperation({
summary: "Get all user subscriptions",
description: "Retrieves all subscriptions/services for the authenticated user",
})
2025-08-22 17:02:49 +09:00
@ApiQuery({
name: "status",
required: false,
type: String,
description: "Filter by subscription status",
})
@ApiResponse({
status: 200,
description: "List of user subscriptions",
type: Object, // Would be SubscriptionList if we had proper DTO decorators
})
async getSubscriptions(
@Request() req: RequestWithUser,
2025-08-22 17:02:49 +09:00
@Query("status") status?: string
): Promise<SubscriptionList | Subscription[]> {
// Validate status if provided
2025-08-22 17:02:49 +09:00
if (status && !["Active", "Suspended", "Terminated", "Cancelled", "Pending"].includes(status)) {
throw new BadRequestException("Invalid status filter");
}
if (status) {
2025-08-22 17:02:49 +09:00
const subscriptions = await this.subscriptionsService.getSubscriptionsByStatus(
2025-08-27 10:54:05 +09:00
req.user.id,
2025-08-22 17:02:49 +09:00
status
);
return subscriptions;
}
2025-08-22 17:02:49 +09:00
2025-08-27 10:54:05 +09:00
return this.subscriptionsService.getSubscriptions(req.user.id);
}
2025-08-22 17:02:49 +09:00
@Get("active")
@ApiOperation({
summary: "Get active subscriptions only",
description: "Retrieves only active subscriptions for the authenticated user",
})
2025-08-22 17:02:49 +09:00
@ApiResponse({
status: 200,
description: "List of active subscriptions",
type: [Object], // Would be Subscription[] if we had proper DTO decorators
})
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")
@ApiOperation({
summary: "Get subscription statistics",
description: "Retrieves subscription count statistics by status",
})
2025-08-22 17:02:49 +09:00
@ApiResponse({
status: 200,
description: "Subscription statistics",
type: Object,
})
async getSubscriptionStats(@Request() req: RequestWithUser): Promise<{
total: number;
active: number;
suspended: number;
cancelled: number;
pending: number;
}> {
2025-08-27 10:54:05 +09:00
return this.subscriptionsService.getSubscriptionStats(req.user.id);
}
2025-08-22 17:02:49 +09:00
@Get(":id")
@ApiOperation({
summary: "Get subscription details by ID",
description: "Retrieves detailed information for a specific subscription",
})
2025-08-22 17:02:49 +09:00
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiResponse({
status: 200,
description: "Subscription details",
type: Object, // Would be Subscription if we had proper DTO decorators
})
2025-08-22 17:02:49 +09:00
@ApiResponse({ status: 404, description: "Subscription not found" })
async getSubscriptionById(
@Request() req: RequestWithUser,
2025-08-22 17:02:49 +09:00
@Param("id", ParseIntPipe) subscriptionId: number
): Promise<Subscription> {
if (subscriptionId <= 0) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Subscription ID must be a positive number");
}
2025-08-22 17:02:49 +09:00
2025-08-27 10:54:05 +09:00
return this.subscriptionsService.getSubscriptionById(req.user.id, subscriptionId);
}
2025-08-22 17:02:49 +09:00
@Get(":id/invoices")
@ApiOperation({
summary: "Get invoices for a specific subscription",
description: "Retrieves all invoices related to a specific subscription",
})
2025-08-22 17:02:49 +09:00
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiQuery({
name: "page",
required: false,
type: Number,
description: "Page number (default: 1)",
})
@ApiQuery({
name: "limit",
required: false,
type: Number,
description: "Items per page (default: 10)",
})
@ApiResponse({
status: 200,
description: "List of invoices for the subscription",
type: Object, // Would be InvoiceList if we had proper DTO decorators
})
2025-08-22 17:02:49 +09:00
@ApiResponse({ status: 404, description: "Subscription not found" })
async getSubscriptionInvoices(
@Request() req: RequestWithUser,
2025-08-22 17:02:49 +09:00
@Param("id", ParseIntPipe) subscriptionId: number,
@Query("page") page?: string,
@Query("limit") limit?: string
): Promise<InvoiceList> {
if (subscriptionId <= 0) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Subscription ID must be a positive number");
}
// Validate and sanitize input
2025-08-22 17:02:49 +09:00
const pageNum = this.validatePositiveInteger(page, 1, "page");
const limitNum = this.validatePositiveInteger(limit, 10, "limit");
// Limit max page size for performance
if (limitNum > 100) {
2025-08-22 17:02:49 +09:00
throw new BadRequestException("Limit cannot exceed 100 items per page");
}
2025-08-22 17:02:49 +09:00
2025-08-27 10:54:05 +09:00
return this.subscriptionsService.getSubscriptionInvoices(req.user.id, subscriptionId, {
2025-08-22 17:02:49 +09:00
page: pageNum,
limit: limitNum,
});
}
2025-08-22 17:02:49 +09:00
private validatePositiveInteger(
value: string | undefined,
defaultValue: number,
fieldName: string
): number {
if (!value) {
return defaultValue;
}
const parsed = parseInt(value, 10);
if (isNaN(parsed) || parsed <= 0) {
throw new BadRequestException(`${fieldName} must be a positive integer`);
}
return parsed;
}
// ==================== SIM Management Endpoints ====================
@Get(":id/sim/debug")
@ApiOperation({
summary: "Debug SIM subscription data",
description: "Retrieves subscription data to help debug SIM management issues",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiResponse({ status: 200, description: "Subscription debug data" })
async debugSimSubscription(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number
) {
return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId);
}
@Get(":id/sim")
@ApiOperation({
summary: "Get SIM details and usage",
description: "Retrieves comprehensive SIM information including details and current usage",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiResponse({ status: 200, description: "SIM information" })
@ApiResponse({ status: 400, description: "Not a SIM subscription" })
@ApiResponse({ status: 404, description: "Subscription not found" })
async getSimInfo(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number
) {
return this.simManagementService.getSimInfo(req.user.id, subscriptionId);
}
@Get(":id/sim/details")
@ApiOperation({
summary: "Get SIM details",
description: "Retrieves detailed SIM information including ICCID, plan, status, etc.",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiResponse({ status: 200, description: "SIM details" })
async getSimDetails(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number
) {
return this.simManagementService.getSimDetails(req.user.id, subscriptionId);
}
@Get(":id/sim/usage")
@ApiOperation({
summary: "Get SIM data usage",
description: "Retrieves current data usage and recent usage history",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiResponse({ status: 200, description: "SIM usage data" })
async getSimUsage(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number
) {
return this.simManagementService.getSimUsage(req.user.id, subscriptionId);
}
@Get(":id/sim/top-up-history")
@ApiOperation({
summary: "Get SIM top-up history",
description: "Retrieves data top-up history for the specified date range",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiQuery({ name: "fromDate", description: "Start date (YYYYMMDD)", example: "20240101" })
@ApiQuery({ name: "toDate", description: "End date (YYYYMMDD)", example: "20241231" })
@ApiResponse({ status: 200, description: "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")
@ApiOperation({
summary: "Top up SIM data quota",
description: "Add data quota to the SIM service",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiBody({
description: "Top-up request",
schema: {
type: "object",
properties: {
quotaMb: { type: "number", description: "Quota in MB", example: 1024 },
},
required: ["quotaMb"],
},
})
@ApiResponse({ status: 200, description: "Top-up successful" })
async topUpSim(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number,
@Body() body: {
quotaMb: number;
}
) {
await this.simManagementService.topUpSim(req.user.id, subscriptionId, body);
return { success: true, message: "SIM top-up completed successfully" };
}
@Post(":id/sim/change-plan")
@ApiOperation({
summary: "Change SIM plan",
description: "Change the SIM service plan",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiBody({
description: "Plan change request",
schema: {
type: "object",
properties: {
newPlanCode: { type: "string", description: "New plan code", example: "LTE3G_P01" },
assignGlobalIp: { type: "boolean", description: "Assign global IP address" },
scheduledAt: { type: "string", description: "Schedule for later (YYYYMMDD)", example: "20241225" },
},
required: ["newPlanCode"],
},
})
@ApiResponse({ status: 200, description: "Plan change successful" })
async changeSimPlan(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number,
@Body() body: {
newPlanCode: string;
assignGlobalIp?: boolean;
scheduledAt?: string;
}
) {
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")
@ApiOperation({
summary: "Cancel SIM service",
description: "Cancel the SIM service (immediate or scheduled)",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiBody({
description: "Cancellation request",
schema: {
type: "object",
properties: {
scheduledAt: { type: "string", description: "Schedule cancellation (YYYYMMDD)", example: "20241231" },
},
},
required: false,
})
@ApiResponse({ status: 200, description: "Cancellation successful" })
async cancelSim(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number,
@Body() body: { scheduledAt?: string } = {}
) {
await this.simManagementService.cancelSim(req.user.id, subscriptionId, body);
return { success: true, message: "SIM cancellation completed successfully" };
}
@Post(":id/sim/reissue-esim")
@ApiOperation({
summary: "Reissue eSIM profile",
description: "Reissue a downloadable eSIM profile (eSIM only)",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiResponse({ status: 200, description: "eSIM reissue successful" })
@ApiResponse({ status: 400, description: "Not an eSIM subscription" })
async reissueEsimProfile(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number
) {
await this.simManagementService.reissueEsimProfile(req.user.id, subscriptionId);
return { success: true, message: "eSIM profile reissue completed successfully" };
}
@Post(":id/sim/features")
@ApiOperation({
summary: "Update SIM features",
description: "Enable/disable voicemail, call waiting, international roaming, and switch network type (4G/5G)",
})
@ApiParam({ name: "id", type: Number, description: "Subscription ID" })
@ApiBody({
description: "Features update request",
schema: {
type: "object",
properties: {
voiceMailEnabled: { type: "boolean" },
callWaitingEnabled: { type: "boolean" },
internationalRoamingEnabled: { type: "boolean" },
networkType: { type: "string", enum: ["4G", "5G"] },
},
},
})
@ApiResponse({ status: 200, description: "Features update successful" })
async updateSimFeatures(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number,
@Body()
body: {
voiceMailEnabled?: boolean;
callWaitingEnabled?: boolean;
internationalRoamingEnabled?: boolean;
networkType?: '4G' | '5G';
}
) {
await this.simManagementService.updateSimFeatures(req.user.id, subscriptionId, body);
return { success: true, message: "SIM features updated successfully" };
}
}