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.
This commit is contained in:
barsa 2025-12-26 13:40:10 +09:00
parent a1be0ea527
commit fcc9bc247e
17 changed files with 247 additions and 147 deletions

View File

@ -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<Invoice> {
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<InvoiceSsoLink> {
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<InvoicePaymentLink> {
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
);

View File

@ -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<ApiSuccessAckResponse> {
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<ApiSuccessAckResponse> {
await this.notificationService.dismiss(notificationId, req.user.id);
await this.notificationService.dismiss(params.id, req.user.id);
return { success: true };
}
}

View File

@ -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),

View File

@ -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")

View File

@ -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<InternetEligibilityDetails> {
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

View File

@ -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<Subscription[]> {
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<Subscription> {
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<InvoiceList> {
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<Record<string, unknown>> {
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<SimActionResponse> {
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<SimPlanChangeResult> {
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<SimActionResponse> {
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<SimActionResponse> {
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<SimActionResponse> {
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<ApiSuccessResponse<SimAvailablePlan[]>> {
const plans = await this.simPlanService.getAvailablePlans(req.user.id, subscriptionId);
return { success: true, data: plans };
@Param() params: SubscriptionIdParamDto
): Promise<SimAvailablePlan[]> {
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<SimPlanChangeResult> {
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<ApiSuccessResponse<SimCancellationPreview>> {
@Param() params: SubscriptionIdParamDto
): Promise<SimCancellationPreview> {
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<SimActionResponse> {
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<SimActionResponse> {
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<SubscriptionActionResponse> {
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<ApiSuccessResponse<SimDomesticCallHistoryResponse>> {
): Promise<SimDomesticCallHistoryResponse> {
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<ApiSuccessResponse<SimInternationalCallHistoryResponse>> {
): Promise<SimInternationalCallHistoryResponse> {
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<ApiSuccessResponse<SimSmsHistoryResponse>> {
): Promise<SimSmsHistoryResponse> {
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;
}
}

View File

@ -18,6 +18,7 @@ export type {
InvoiceStatus,
InvoiceItem,
Invoice,
InvoiceIdParam,
InvoicePagination,
InvoiceList,
InvoiceSsoLink,

View File

@ -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<typeof invoiceIdParamSchema>;
// Invoice Pagination Schema
export const invoicePaginationSchema = z.object({
page: z.number().int().nonnegative(),

View File

@ -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";

View File

@ -224,3 +224,12 @@ export const notificationQuerySchema = z.object({
});
export type NotificationQuery = z.infer<typeof notificationQuerySchema>;
// =============================================================================
// Route Param Schemas (BFF)
// =============================================================================
export const notificationIdParamSchema = z.object({
id: z.string().uuid(),
});
export type NotificationIdParam = z.infer<typeof notificationIdParamSchema>;

View File

@ -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
);
/**

View File

@ -29,6 +29,8 @@ export type {
InternetAddonCatalogItem,
InternetEligibilityStatus,
InternetEligibilityDetails,
InternetEligibilityRequest,
InternetEligibilityRequestResponse,
// SIM products
SimCatalogProduct,
SimActivationFeeCatalogItem,

View File

@ -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<typeof internetCatalogCollection
export type InternetEligibilityStatus = z.infer<typeof internetEligibilityStatusSchema>;
export type InternetEligibilityDetails = z.infer<typeof internetEligibilityDetailsSchema>;
export type InternetEligibilityRequest = z.infer<typeof internetEligibilityRequestSchema>;
export type InternetEligibilityRequestResponse = z.infer<
typeof internetEligibilityRequestResponseSchema
>;
// SIM products
export type SimCatalogProduct = z.infer<typeof simCatalogProductSchema>;

View File

@ -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,

View File

@ -176,6 +176,9 @@ export const simAvailablePlanSchema = simCatalogProductSchema.extend({
export type SimAvailablePlan = z.infer<typeof simAvailablePlanSchema>;
export const simAvailablePlanArraySchema = z.array(simAvailablePlanSchema);
export type SimAvailablePlanArray = z.infer<typeof simAvailablePlanArraySchema>;
/**
* Cancellation month option for SIM cancellation preview
*/
@ -252,6 +255,12 @@ export type SimCallHistoryImportResult = z.infer<typeof simCallHistoryImportResu
export const simSftpFilesSchema = z.array(z.string());
export type SimSftpFiles = z.infer<typeof simSftpFilesSchema>;
export const simSftpListResultSchema = z.object({
path: z.string(),
files: simSftpFilesSchema,
});
export type SimSftpListResult = z.infer<typeof simSftpListResultSchema>;
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");

View File

@ -17,7 +17,9 @@ export type {
SubscriptionStatus,
SubscriptionCycle,
Subscription,
SubscriptionArray,
SubscriptionList,
SubscriptionIdParam,
SubscriptionQueryParams,
SubscriptionQuery,
SubscriptionStats,

View File

@ -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<typeof subscriptionIdParamSchema>;
// ============================================================================
// Query Parameter Schemas
// ============================================================================
@ -116,6 +127,7 @@ export const simPlanChangeResultSchema = apiSuccessMessageResponseSchema.extend(
export type SubscriptionStatus = z.infer<typeof subscriptionStatusSchema>;
export type SubscriptionCycle = z.infer<typeof subscriptionCycleSchema>;
export type Subscription = z.infer<typeof subscriptionSchema>;
export type SubscriptionArray = z.infer<typeof subscriptionArraySchema>;
export type SubscriptionList = z.infer<typeof subscriptionListSchema>;
export type SubscriptionStats = z.infer<typeof subscriptionStatsSchema>;
export type SimActionResponse = z.infer<typeof simActionResponseSchema>;