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 { import { Controller, Get, Post, Param, Query, Request, HttpCode, HttpStatus } from "@nestjs/common";
Controller,
Get,
Post,
Param,
Query,
Request,
ParseIntPipe,
HttpCode,
HttpStatus,
} from "@nestjs/common";
import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service.js"; import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service.js";
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service.js"; import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.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 type { Invoice, InvoiceList, InvoiceSsoLink } from "@customer-portal/domain/billing";
import { import {
invoiceIdParamSchema,
invoiceListQuerySchema, invoiceListQuerySchema,
invoiceListSchema, invoiceListSchema,
invoiceSchema, invoiceSchema,
@ -37,6 +28,7 @@ import {
} from "@customer-portal/domain/payments"; } from "@customer-portal/domain/payments";
class InvoiceListQueryDto extends createZodDto(invoiceListQuerySchema) {} class InvoiceListQueryDto extends createZodDto(invoiceListQuerySchema) {}
class InvoiceIdParamDto extends createZodDto(invoiceIdParamSchema) {}
class InvoiceListDto extends createZodDto(invoiceListSchema) {} class InvoiceListDto extends createZodDto(invoiceListSchema) {}
class InvoiceDto extends createZodDto(invoiceSchema) {} class InvoiceDto extends createZodDto(invoiceSchema) {}
class InvoiceSsoLinkDto extends createZodDto(invoiceSsoLinkSchema) {} class InvoiceSsoLinkDto extends createZodDto(invoiceSsoLinkSchema) {}
@ -104,15 +96,15 @@ export class InvoicesController {
@ZodResponse({ description: "Get invoice by id", type: InvoiceDto }) @ZodResponse({ description: "Get invoice by id", type: InvoiceDto })
async getInvoiceById( async getInvoiceById(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) invoiceId: number @Param() params: InvoiceIdParamDto
): Promise<Invoice> { ): Promise<Invoice> {
return this.invoicesService.getInvoiceById(req.user.id, invoiceId); return this.invoicesService.getInvoiceById(req.user.id, params.id);
} }
@Get(":id/subscriptions") @Get(":id/subscriptions")
getInvoiceSubscriptions( getInvoiceSubscriptions(
@Request() _req: RequestWithUser, @Request() _req: RequestWithUser,
@Param("id", ParseIntPipe) _invoiceId: number @Param() _params: InvoiceIdParamDto
): Subscription[] { ): Subscription[] {
// This functionality has been moved to WHMCS directly // This functionality has been moved to WHMCS directly
// For now, return empty array as subscriptions are managed in WHMCS // 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 }) @ZodResponse({ description: "Create invoice SSO link", type: InvoiceSsoLinkDto })
async createSsoLink( async createSsoLink(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) invoiceId: number, @Param() params: InvoiceIdParamDto,
@Query() query: InvoiceSsoQueryDto @Query() query: InvoiceSsoQueryDto
): Promise<InvoiceSsoLink> { ): Promise<InvoiceSsoLink> {
const mapping = await this.mappingsService.findByUserId(req.user.id); const mapping = await this.mappingsService.findByUserId(req.user.id);
@ -136,7 +128,7 @@ export class InvoicesController {
const ssoUrl = await this.whmcsService.whmcsSsoForInvoice( const ssoUrl = await this.whmcsService.whmcsSsoForInvoice(
mapping.whmcsClientId, mapping.whmcsClientId,
invoiceId, params.id,
parsedQuery.target parsedQuery.target
); );
@ -151,7 +143,7 @@ export class InvoicesController {
@ZodResponse({ description: "Create invoice payment link", type: InvoicePaymentLinkDto }) @ZodResponse({ description: "Create invoice payment link", type: InvoicePaymentLinkDto })
async createPaymentLink( async createPaymentLink(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) invoiceId: number, @Param() params: InvoiceIdParamDto,
@Query() query: InvoicePaymentLinkQueryDto @Query() query: InvoicePaymentLinkQueryDto
): Promise<InvoicePaymentLink> { ): Promise<InvoicePaymentLink> {
const mapping = await this.mappingsService.findByUserId(req.user.id); const mapping = await this.mappingsService.findByUserId(req.user.id);
@ -163,7 +155,7 @@ export class InvoicesController {
const ssoResult = await this.whmcsService.createPaymentSsoToken( const ssoResult = await this.whmcsService.createPaymentSsoToken(
mapping.whmcsClientId, mapping.whmcsClientId,
invoiceId, params.id,
parsedQuery.paymentMethodId, parsedQuery.paymentMethodId,
parsedQuery.gatewayName parsedQuery.gatewayName
); );

View File

@ -11,6 +11,7 @@ import { NotificationService } from "./notifications.service.js";
import { import {
notificationListResponseSchema, notificationListResponseSchema,
notificationUnreadCountResponseSchema, notificationUnreadCountResponseSchema,
notificationIdParamSchema,
type NotificationListResponse, type NotificationListResponse,
} from "@customer-portal/domain/notifications"; } from "@customer-portal/domain/notifications";
import { notificationQuerySchema } from "@customer-portal/domain/notifications"; import { notificationQuerySchema } from "@customer-portal/domain/notifications";
@ -21,6 +22,7 @@ import {
import { createZodDto, ZodResponse } from "nestjs-zod"; import { createZodDto, ZodResponse } from "nestjs-zod";
class NotificationQueryDto extends createZodDto(notificationQuerySchema) {} class NotificationQueryDto extends createZodDto(notificationQuerySchema) {}
class NotificationIdParamDto extends createZodDto(notificationIdParamSchema) {}
class NotificationListResponseDto extends createZodDto(notificationListResponseSchema) {} class NotificationListResponseDto extends createZodDto(notificationListResponseSchema) {}
class NotificationUnreadCountResponseDto extends createZodDto( class NotificationUnreadCountResponseDto extends createZodDto(
notificationUnreadCountResponseSchema notificationUnreadCountResponseSchema
@ -70,9 +72,9 @@ export class NotificationsController {
@ZodResponse({ description: "Mark as read", type: ApiSuccessAckResponseDto }) @ZodResponse({ description: "Mark as read", type: ApiSuccessAckResponseDto })
async markAsRead( async markAsRead(
@Req() req: RequestWithUser, @Req() req: RequestWithUser,
@Param("id") notificationId: string @Param() params: NotificationIdParamDto
): Promise<ApiSuccessAckResponse> { ): Promise<ApiSuccessAckResponse> {
await this.notificationService.markAsRead(notificationId, req.user.id); await this.notificationService.markAsRead(params.id, req.user.id);
return { success: true }; return { success: true };
} }
@ -95,9 +97,9 @@ export class NotificationsController {
@ZodResponse({ description: "Dismiss notification", type: ApiSuccessAckResponseDto }) @ZodResponse({ description: "Dismiss notification", type: ApiSuccessAckResponseDto })
async dismiss( async dismiss(
@Req() req: RequestWithUser, @Req() req: RequestWithUser,
@Param("id") notificationId: string @Param() params: NotificationIdParamDto
): Promise<ApiSuccessAckResponse> { ): Promise<ApiSuccessAckResponse> {
await this.notificationService.dismiss(notificationId, req.user.id); await this.notificationService.dismiss(params.id, req.user.id);
return { success: true }; return { success: true };
} }
} }

View File

@ -7,10 +7,9 @@ import { CheckoutSessionService } from "../services/checkout-session.service.js"
import { import {
checkoutCartSchema, checkoutCartSchema,
checkoutBuildCartRequestSchema, checkoutBuildCartRequestSchema,
checkoutBuildCartResponseSchema,
checkoutSessionIdParamSchema, checkoutSessionIdParamSchema,
checkoutSessionResponseSchema, checkoutSessionDataSchema,
checkoutValidateCartResponseSchema, checkoutValidateCartDataSchema,
} from "@customer-portal/domain/orders"; } from "@customer-portal/domain/orders";
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.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 CheckoutBuildCartRequestDto extends createZodDto(checkoutBuildCartRequestSchema) {}
class CheckoutSessionIdParamDto extends createZodDto(checkoutSessionIdParamSchema) {} class CheckoutSessionIdParamDto extends createZodDto(checkoutSessionIdParamSchema) {}
class CheckoutCartDto extends createZodDto(checkoutCartSchema) {} class CheckoutCartDto extends createZodDto(checkoutCartSchema) {}
class CheckoutBuildCartResponseDto extends createZodDto(checkoutBuildCartResponseSchema) {} class CheckoutBuildCartResponseDto extends createZodDto(checkoutCartSchema) {}
class CheckoutSessionResponseDto extends createZodDto(checkoutSessionResponseSchema) {} class CheckoutSessionResponseDto extends createZodDto(checkoutSessionDataSchema) {}
class ValidateCartResponseDto extends createZodDto(checkoutValidateCartResponseSchema) {} class ValidateCartResponseDto extends createZodDto(checkoutValidateCartDataSchema) {}
@Controller("checkout") @Controller("checkout")
@Public() // Cart building and validation can be done without authentication @Public() // Cart building and validation can be done without authentication
@ -52,7 +51,7 @@ export class CheckoutController {
req.user?.id req.user?.id
); );
return { success: true as const, data: cart }; return cart;
} catch (error) { } catch (error) {
this.logger.error("Failed to build checkout cart", { this.logger.error("Failed to build checkout cart", {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
@ -90,15 +89,12 @@ export class CheckoutController {
const session = await this.checkoutSessions.createSession(body, cart); const session = await this.checkoutSessions.createSession(body, cart);
return { return {
success: true as const, sessionId: session.sessionId,
data: { expiresAt: session.expiresAt,
sessionId: session.sessionId, orderType: body.orderType,
expiresAt: session.expiresAt, cart: {
orderType: body.orderType, items: cart.items,
cart: { totals: cart.totals,
items: cart.items,
totals: cart.totals,
},
}, },
}; };
} }
@ -113,15 +109,12 @@ export class CheckoutController {
async getSession(@Param() params: CheckoutSessionIdParamDto) { async getSession(@Param() params: CheckoutSessionIdParamDto) {
const session = await this.checkoutSessions.getSession(params.sessionId); const session = await this.checkoutSessions.getSession(params.sessionId);
return { return {
success: true as const, sessionId: params.sessionId,
data: { expiresAt: session.expiresAt,
sessionId: params.sessionId, orderType: session.request.orderType,
expiresAt: session.expiresAt, cart: {
orderType: session.request.orderType, items: session.cart.items,
cart: { totals: session.cart.totals,
items: session.cart.items,
totals: session.cart.totals,
},
}, },
}; };
} }
@ -140,7 +133,7 @@ export class CheckoutController {
try { try {
this.checkoutService.validateCart(cart); this.checkoutService.validateCart(cart);
return { success: true as const, data: { valid: true } }; return { valid: true };
} catch (error) { } catch (error) {
this.logger.error("Checkout cart validation failed", { this.logger.error("Checkout cart validation failed", {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),

View File

@ -25,7 +25,6 @@ import {
orderListResponseSchema, orderListResponseSchema,
type CreateOrderRequest, type CreateOrderRequest,
} from "@customer-portal/domain/orders"; } from "@customer-portal/domain/orders";
import { apiSuccessResponseSchema } from "@customer-portal/domain/common";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { OrderEventsService } from "./services/order-events.service.js"; import { OrderEventsService } from "./services/order-events.service.js";
import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.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 CreateOrderRequestDto extends createZodDto(createOrderRequestSchema) {}
class CheckoutSessionCreateOrderDto extends createZodDto(checkoutSessionCreateOrderRequestSchema) {} class CheckoutSessionCreateOrderDto extends createZodDto(checkoutSessionCreateOrderRequestSchema) {}
class SfOrderIdParamDto extends createZodDto(sfOrderIdParamSchema) {} class SfOrderIdParamDto extends createZodDto(sfOrderIdParamSchema) {}
class CreateOrderResponseDto extends createZodDto( class CreateOrderResponseDto extends createZodDto(orderCreateResponseSchema) {}
apiSuccessResponseSchema(orderCreateResponseSchema)
) {}
class OrderDetailsDto extends createZodDto(orderDetailsSchema) {} class OrderDetailsDto extends createZodDto(orderDetailsSchema) {}
class OrderListResponseDto extends createZodDto(orderListResponseSchema) {} class OrderListResponseDto extends createZodDto(orderListResponseSchema) {}
@ -70,7 +67,7 @@ export class OrdersController {
try { try {
const result = await this.orderOrchestrator.createOrder(req.user.id, body); const result = await this.orderOrchestrator.createOrder(req.user.id, body);
return { success: true as const, data: result }; return result;
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
{ {
@ -137,7 +134,7 @@ export class OrdersController {
await this.checkoutSessions.deleteSession(body.checkoutSessionId); await this.checkoutSessions.deleteSession(body.checkoutSessionId);
return { success: true as const, data: result }; return result;
} }
@Get("user") @Get("user")

View File

@ -1,12 +1,22 @@
import { Body, Controller, Get, Header, Post, Req, UseGuards } from "@nestjs/common"; 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 type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js"; import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
import { InternetServicesService } from "./services/internet-services.service.js"; import { InternetServicesService } from "./services/internet-services.service.js";
import type { InternetEligibilityDetails } from "@customer-portal/domain/services"; 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 EligibilityRequestDto extends createZodDto(internetEligibilityRequestSchema) {}
class InternetEligibilityDetailsResponseDto extends createZodDto(
internetEligibilityDetailsSchema
) {}
class InternetEligibilityRequestResponseDto extends createZodDto(
internetEligibilityRequestResponseSchema
) {}
/** /**
* Internet Eligibility Controller * Internet Eligibility Controller
@ -26,6 +36,10 @@ export class InternetEligibilityController {
@Get("eligibility") @Get("eligibility")
@RateLimit({ limit: 60, ttl: 60 }) // 60/min per IP (cheap) @RateLimit({ limit: 60, ttl: 60 }) // 60/min per IP (cheap)
@Header("Cache-Control", "private, no-store") @Header("Cache-Control", "private, no-store")
@ZodResponse({
description: "Get internet eligibility",
type: InternetEligibilityDetailsResponseDto,
})
async getEligibility(@Req() req: RequestWithUser): Promise<InternetEligibilityDetails> { async getEligibility(@Req() req: RequestWithUser): Promise<InternetEligibilityDetails> {
return this.internetCatalog.getEligibilityDetailsForUser(req.user.id); return this.internetCatalog.getEligibilityDetailsForUser(req.user.id);
} }
@ -33,6 +47,10 @@ export class InternetEligibilityController {
@Post("eligibility-request") @Post("eligibility-request")
@RateLimit({ limit: 5, ttl: 300 }) // 5 per 5 minutes per IP @RateLimit({ limit: 5, ttl: 300 }) // 5 per 5 minutes per IP
@Header("Cache-Control", "private, no-store") @Header("Cache-Control", "private, no-store")
@ZodResponse({
description: "Request internet eligibility check",
type: InternetEligibilityRequestResponseDto,
})
async requestEligibility( async requestEligibility(
@Req() req: RequestWithUser, @Req() req: RequestWithUser,
@Body() body: EligibilityRequestDto @Body() body: EligibilityRequestDto

View File

@ -6,7 +6,6 @@ import {
Query, Query,
Body, Body,
Request, Request,
ParseIntPipe,
Header, Header,
UseGuards, UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
@ -18,6 +17,8 @@ import { SimTopUpPricingService } from "./sim-management/services/sim-topup-pric
import { import {
subscriptionQuerySchema, subscriptionQuerySchema,
subscriptionListSchema, subscriptionListSchema,
subscriptionArraySchema,
subscriptionIdParamSchema,
subscriptionSchema, subscriptionSchema,
subscriptionStatsSchema, subscriptionStatsSchema,
simActionResponseSchema, simActionResponseSchema,
@ -32,8 +33,6 @@ import type {
SimPlanChangeResult, SimPlanChangeResult,
} from "@customer-portal/domain/subscriptions"; } from "@customer-portal/domain/subscriptions";
import type { InvoiceList } from "@customer-portal/domain/billing"; 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 { createPaginationSchema } from "@customer-portal/domain/toolkit/validation/helpers";
import { import {
simTopupRequestSchema, simTopupRequestSchema,
@ -47,12 +46,22 @@ import {
simHistoryQuerySchema, simHistoryQuerySchema,
simSftpListQuerySchema, simSftpListQuerySchema,
simCallHistoryImportQuerySchema, simCallHistoryImportQuerySchema,
simHistoryAvailableMonthsSchema,
simSftpListResultSchema,
simCallHistoryImportResultSchema,
simTopUpPricingSchema,
simTopUpPricingPreviewRequestSchema, simTopUpPricingPreviewRequestSchema,
simTopUpPricingPreviewResponseSchema,
simReissueEsimRequestSchema, simReissueEsimRequestSchema,
simInfoSchema, simInfoSchema,
simDetailsSchema, simDetailsSchema,
simUsageSchema, simUsageSchema,
simTopUpHistorySchema, simTopUpHistorySchema,
simAvailablePlanArraySchema,
simCancellationPreviewSchema,
simDomesticCallHistoryResponseSchema,
simInternationalCallHistoryResponseSchema,
simSmsHistoryResponseSchema,
type SimAvailablePlan, type SimAvailablePlan,
type SimCancellationPreview, type SimCancellationPreview,
type SimDomesticCallHistoryResponse, type SimDomesticCallHistoryResponse,
@ -81,6 +90,7 @@ const subscriptionInvoiceQuerySchema = createPaginationSchema({
class SubscriptionQueryDto extends createZodDto(subscriptionQuerySchema) {} class SubscriptionQueryDto extends createZodDto(subscriptionQuerySchema) {}
class SubscriptionInvoiceQueryDto extends createZodDto(subscriptionInvoiceQuerySchema) {} class SubscriptionInvoiceQueryDto extends createZodDto(subscriptionInvoiceQuerySchema) {}
class SubscriptionIdParamDto extends createZodDto(subscriptionIdParamSchema) {}
class SimTopupRequestDto extends createZodDto(simTopupRequestSchema) {} class SimTopupRequestDto extends createZodDto(simTopupRequestSchema) {}
class SimChangePlanRequestDto extends createZodDto(simChangePlanRequestSchema) {} class SimChangePlanRequestDto extends createZodDto(simChangePlanRequestSchema) {}
class SimCancelRequestDto extends createZodDto(simCancelRequestSchema) {} class SimCancelRequestDto extends createZodDto(simCancelRequestSchema) {}
@ -103,15 +113,35 @@ class SimUsageDto extends createZodDto(simUsageSchema) {}
class SimTopUpHistoryDto extends createZodDto(simTopUpHistorySchema) {} class SimTopUpHistoryDto extends createZodDto(simTopUpHistorySchema) {}
class SubscriptionListDto extends createZodDto(subscriptionListSchema) {} class SubscriptionListDto extends createZodDto(subscriptionListSchema) {}
class ActiveSubscriptionsDto extends createZodDto(subscriptionArraySchema) {}
class SubscriptionDto extends createZodDto(subscriptionSchema) {} class SubscriptionDto extends createZodDto(subscriptionSchema) {}
class SubscriptionStatsDto extends createZodDto(subscriptionStatsSchema) {} class SubscriptionStatsDto extends createZodDto(subscriptionStatsSchema) {}
class SimActionResponseDto extends createZodDto(simActionResponseSchema) {} class SimActionResponseDto extends createZodDto(simActionResponseSchema) {}
class SimPlanChangeResultDto extends createZodDto(simPlanChangeResultSchema) {} class SimPlanChangeResultDto extends createZodDto(simPlanChangeResultSchema) {}
class InvoiceListDto extends createZodDto(invoiceListSchema) {} class InvoiceListDto extends createZodDto(invoiceListSchema) {}
class InternetCancellationPreviewResponseDto extends createZodDto( 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") @Controller("subscriptions")
export class SubscriptionsController { export class SubscriptionsController {
constructor( constructor(
@ -138,7 +168,7 @@ export class SubscriptionsController {
@Get("active") @Get("active")
@Header("Cache-Control", "private, max-age=300") // 5 minutes, user-specific @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[]> { async getActiveSubscriptions(@Request() req: RequestWithUser): Promise<Subscription[]> {
return this.subscriptionsService.getActiveSubscriptions(req.user.id); return this.subscriptionsService.getActiveSubscriptions(req.user.id);
} }
@ -158,9 +188,13 @@ export class SubscriptionsController {
@Public() @Public()
@Get("sim/call-history/available-months") @Get("sim/call-history/available-months")
@Header("Cache-Control", "public, max-age=3600") @Header("Cache-Control", "public, max-age=3600")
@ZodResponse({
description: "Get available call/SMS history months",
type: SimHistoryAvailableMonthsResponseDto,
})
async getAvailableMonths() { async getAvailableMonths() {
const months = await this.simCallHistoryService.getAvailableMonths(); const months = await this.simCallHistoryService.getAvailableMonths();
return { success: true, data: months }; return months;
} }
/** /**
@ -168,10 +202,11 @@ export class SubscriptionsController {
*/ */
@UseGuards(AdminGuard) @UseGuards(AdminGuard)
@Get("sim/call-history/sftp-files") @Get("sim/call-history/sftp-files")
@ZodResponse({ description: "List available SFTP files", type: SimSftpListResultResponseDto })
async listSftpFiles(@Query() query: SimSftpListQueryDto) { async listSftpFiles(@Query() query: SimSftpListQueryDto) {
const parsedQuery = simSftpListQuerySchema.parse(query as unknown); const parsedQuery = simSftpListQuerySchema.parse(query as unknown);
const files = await this.simCallHistoryService.listSftpFiles(parsedQuery.path); 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) @UseGuards(AdminGuard)
@Post("sim/call-history/import") @Post("sim/call-history/import")
@ZodResponse({
description: "Import call history (admin)",
type: SimCallHistoryImportResultResponseDto,
})
async importCallHistory(@Query() query: SimCallHistoryImportQueryDto) { async importCallHistory(@Query() query: SimCallHistoryImportQueryDto) {
const parsedQuery = simCallHistoryImportQuerySchema.parse(query as unknown); const parsedQuery = simCallHistoryImportQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.importCallHistory(parsedQuery.month); const result = await this.simCallHistoryService.importCallHistory(parsedQuery.month);
return { return result;
success: true,
message: `Imported ${result.domestic} domestic calls, ${result.international} international calls, ${result.sms} SMS`,
data: result,
};
} }
@Get("sim/top-up/pricing") @Get("sim/top-up/pricing")
@Header("Cache-Control", "public, max-age=3600") // 1 hour, pricing is relatively static @Header("Cache-Control", "public, max-age=3600") // 1 hour, pricing is relatively static
@ZodResponse({ description: "Get SIM top-up pricing", type: SimTopUpPricingResponseDto })
async getSimTopUpPricing() { async getSimTopUpPricing() {
const pricing = await this.simTopUpPricingService.getTopUpPricing(); const pricing = await this.simTopUpPricingService.getTopUpPricing();
return { success: true, data: pricing }; return pricing;
} }
@Get("sim/top-up/pricing/preview") @Get("sim/top-up/pricing/preview")
@Header("Cache-Control", "public, max-age=3600") // 1 hour, pricing calculation is deterministic @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) { async previewSimTopUpPricing(@Query() query: SimTopUpPricingPreviewRequestDto) {
const preview = await this.simTopUpPricingService.calculatePricingPreview(query.quotaMb); const preview = await this.simTopUpPricingService.calculatePricingPreview(query.quotaMb);
return { success: true, data: preview }; return preview;
} }
@Get("debug/sim-details/:account") @Get("debug/sim-details/:account")
@ -216,9 +256,9 @@ export class SubscriptionsController {
@ZodResponse({ description: "Get subscription", type: SubscriptionDto }) @ZodResponse({ description: "Get subscription", type: SubscriptionDto })
async getSubscriptionById( async getSubscriptionById(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number @Param() params: SubscriptionIdParamDto
): Promise<Subscription> { ): Promise<Subscription> {
return this.subscriptionsService.getSubscriptionById(req.user.id, subscriptionId); return this.subscriptionsService.getSubscriptionById(req.user.id, params.id);
} }
@Get(":id/invoices") @Get(":id/invoices")
@ -226,10 +266,10 @@ export class SubscriptionsController {
@ZodResponse({ description: "Get subscription invoices", type: InvoiceListDto }) @ZodResponse({ description: "Get subscription invoices", type: InvoiceListDto })
async getSubscriptionInvoices( async getSubscriptionInvoices(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Query() query: SubscriptionInvoiceQueryDto @Query() query: SubscriptionInvoiceQueryDto
): Promise<InvoiceList> { ): 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) ==================== // ==================== SIM Management Endpoints (subscription-specific) ====================
@ -238,56 +278,47 @@ export class SubscriptionsController {
@UseGuards(AdminGuard) @UseGuards(AdminGuard)
async debugSimSubscription( async debugSimSubscription(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number @Param() params: SubscriptionIdParamDto
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
return this.simManagementService.debugSimSubscription(req.user.id, subscriptionId); return this.simManagementService.debugSimSubscription(req.user.id, params.id);
} }
@Get(":id/sim") @Get(":id/sim")
@ZodResponse({ description: "Get SIM info", type: SimInfoDto }) @ZodResponse({ description: "Get SIM info", type: SimInfoDto })
async getSimInfo( async getSimInfo(@Request() req: RequestWithUser, @Param() params: SubscriptionIdParamDto) {
@Request() req: RequestWithUser, return this.simManagementService.getSimInfo(req.user.id, params.id);
@Param("id", ParseIntPipe) subscriptionId: number
) {
return this.simManagementService.getSimInfo(req.user.id, subscriptionId);
} }
@Get(":id/sim/details") @Get(":id/sim/details")
@ZodResponse({ description: "Get SIM details", type: SimDetailsDto }) @ZodResponse({ description: "Get SIM details", type: SimDetailsDto })
async getSimDetails( async getSimDetails(@Request() req: RequestWithUser, @Param() params: SubscriptionIdParamDto) {
@Request() req: RequestWithUser, return this.simManagementService.getSimDetails(req.user.id, params.id);
@Param("id", ParseIntPipe) subscriptionId: number
) {
return this.simManagementService.getSimDetails(req.user.id, subscriptionId);
} }
@Get(":id/sim/usage") @Get(":id/sim/usage")
@ZodResponse({ description: "Get SIM usage", type: SimUsageDto }) @ZodResponse({ description: "Get SIM usage", type: SimUsageDto })
async getSimUsage( async getSimUsage(@Request() req: RequestWithUser, @Param() params: SubscriptionIdParamDto) {
@Request() req: RequestWithUser, return this.simManagementService.getSimUsage(req.user.id, params.id);
@Param("id", ParseIntPipe) subscriptionId: number
) {
return this.simManagementService.getSimUsage(req.user.id, subscriptionId);
} }
@Get(":id/sim/top-up-history") @Get(":id/sim/top-up-history")
@ZodResponse({ description: "Get SIM top-up history", type: SimTopUpHistoryDto }) @ZodResponse({ description: "Get SIM top-up history", type: SimTopUpHistoryDto })
async getSimTopUpHistory( async getSimTopUpHistory(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Query() query: SimTopUpHistoryRequestDto @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") @Post(":id/sim/top-up")
@ZodResponse({ description: "Top up SIM", type: SimActionResponseDto }) @ZodResponse({ description: "Top up SIM", type: SimActionResponseDto })
async topUpSim( async topUpSim(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimTopupRequestDto @Body() body: SimTopupRequestDto
): Promise<SimActionResponse> { ): 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" }; return { success: true, message: "SIM top-up completed successfully" };
} }
@ -295,10 +326,10 @@ export class SubscriptionsController {
@ZodResponse({ description: "Change SIM plan", type: SimPlanChangeResultDto }) @ZodResponse({ description: "Change SIM plan", type: SimPlanChangeResultDto })
async changeSimPlan( async changeSimPlan(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimChangePlanRequestDto @Body() body: SimChangePlanRequestDto
): Promise<SimPlanChangeResult> { ): 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 { return {
success: true, success: true,
message: "SIM plan change completed successfully", message: "SIM plan change completed successfully",
@ -310,25 +341,22 @@ export class SubscriptionsController {
@ZodResponse({ description: "Cancel SIM", type: SimActionResponseDto }) @ZodResponse({ description: "Cancel SIM", type: SimActionResponseDto })
async cancelSim( async cancelSim(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimCancelRequestDto @Body() body: SimCancelRequestDto
): Promise<SimActionResponse> { ): 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" }; return { success: true, message: "SIM cancellation completed successfully" };
} }
@Post(":id/sim/reissue-esim") @Post(":id/sim/reissue-esim")
@ZodResponse({ description: "Reissue eSIM profile", type: SimActionResponseDto })
async reissueEsimProfile( async reissueEsimProfile(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimReissueEsimRequestDto @Body() body: SimReissueEsimRequestDto
): Promise<SimActionResponse> { ): Promise<SimActionResponse> {
const parsedBody = simReissueEsimRequestSchema.parse(body as unknown); const parsedBody = simReissueEsimRequestSchema.parse(body as unknown);
await this.simManagementService.reissueEsimProfile( await this.simManagementService.reissueEsimProfile(req.user.id, params.id, parsedBody.newEid);
req.user.id,
subscriptionId,
parsedBody.newEid
);
return { success: true, message: "eSIM profile reissue completed successfully" }; return { success: true, message: "eSIM profile reissue completed successfully" };
} }
@ -336,10 +364,10 @@ export class SubscriptionsController {
@ZodResponse({ description: "Update SIM features", type: SimActionResponseDto }) @ZodResponse({ description: "Update SIM features", type: SimActionResponseDto })
async updateSimFeatures( async updateSimFeatures(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimFeaturesRequestDto @Body() body: SimFeaturesRequestDto
): Promise<SimActionResponse> { ): 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" }; return { success: true, message: "SIM features updated successfully" };
} }
@ -350,12 +378,13 @@ export class SubscriptionsController {
*/ */
@Get(":id/sim/available-plans") @Get(":id/sim/available-plans")
@Header("Cache-Control", "private, max-age=300") @Header("Cache-Control", "private, max-age=300")
@ZodResponse({ description: "Get available SIM plans", type: SimAvailablePlansResponseDto })
async getAvailablePlans( async getAvailablePlans(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number @Param() params: SubscriptionIdParamDto
): Promise<ApiSuccessResponse<SimAvailablePlan[]>> { ): Promise<SimAvailablePlan[]> {
const plans = await this.simPlanService.getAvailablePlans(req.user.id, subscriptionId); const plans = await this.simPlanService.getAvailablePlans(req.user.id, params.id);
return { success: true, data: plans }; return plans;
} }
/** /**
@ -365,10 +394,10 @@ export class SubscriptionsController {
@ZodResponse({ description: "Change SIM plan (full)", type: SimPlanChangeResultDto }) @ZodResponse({ description: "Change SIM plan (full)", type: SimPlanChangeResultDto })
async changeSimPlanFull( async changeSimPlanFull(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimChangePlanFullRequestDto @Body() body: SimChangePlanFullRequestDto
): Promise<SimPlanChangeResult> { ): 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 { return {
success: true, success: true,
message: `SIM plan change scheduled for ${result.scheduledAt}`, message: `SIM plan change scheduled for ${result.scheduledAt}`,
@ -381,15 +410,19 @@ export class SubscriptionsController {
*/ */
@Get(":id/sim/cancellation-preview") @Get(":id/sim/cancellation-preview")
@Header("Cache-Control", "private, max-age=60") @Header("Cache-Control", "private, max-age=60")
@ZodResponse({
description: "Get SIM cancellation preview",
type: SimCancellationPreviewResponseDto,
})
async getCancellationPreview( async getCancellationPreview(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number @Param() params: SubscriptionIdParamDto
): Promise<ApiSuccessResponse<SimCancellationPreview>> { ): Promise<SimCancellationPreview> {
const preview = await this.simCancellationService.getCancellationPreview( const preview = await this.simCancellationService.getCancellationPreview(
req.user.id, 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 }) @ZodResponse({ description: "Cancel SIM (full)", type: SimActionResponseDto })
async cancelSimFull( async cancelSimFull(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimCancelFullRequestDto @Body() body: SimCancelFullRequestDto
): Promise<SimActionResponse> { ): Promise<SimActionResponse> {
await this.simCancellationService.cancelSimFull(req.user.id, subscriptionId, body); await this.simCancellationService.cancelSimFull(req.user.id, params.id, body);
return { return {
success: true, success: true,
message: `SIM cancellation scheduled for end of ${body.cancellationMonth}`, message: `SIM cancellation scheduled for end of ${body.cancellationMonth}`,
@ -416,10 +449,10 @@ export class SubscriptionsController {
@ZodResponse({ description: "Reissue SIM", type: SimActionResponseDto }) @ZodResponse({ description: "Reissue SIM", type: SimActionResponseDto })
async reissueSim( async reissueSim(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: SimReissueFullRequestDto @Body() body: SimReissueFullRequestDto
): Promise<SimActionResponse> { ): 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") { if (body.simType === "esim") {
return { success: true, message: "eSIM profile reissue request submitted" }; return { success: true, message: "eSIM profile reissue request submitted" };
@ -444,13 +477,13 @@ export class SubscriptionsController {
}) })
async getInternetCancellationPreview( async getInternetCancellationPreview(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number @Param() params: SubscriptionIdParamDto
) { ) {
const preview = await this.internetCancellationService.getCancellationPreview( const preview = await this.internetCancellationService.getCancellationPreview(
req.user.id, 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 }) @ZodResponse({ description: "Cancel internet", type: SimActionResponseDto })
async cancelInternet( async cancelInternet(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Body() body: InternetCancelRequestDto @Body() body: InternetCancelRequestDto
): Promise<SubscriptionActionResponse> { ): Promise<SubscriptionActionResponse> {
await this.internetCancellationService.submitCancellation(req.user.id, subscriptionId, body); await this.internetCancellationService.submitCancellation(req.user.id, params.id, body);
return { return {
success: true, success: true,
message: `Internet cancellation scheduled for end of ${body.cancellationMonth}`, message: `Internet cancellation scheduled for end of ${body.cancellationMonth}`,
@ -477,20 +510,24 @@ export class SubscriptionsController {
*/ */
@Get(":id/sim/call-history/domestic") @Get(":id/sim/call-history/domestic")
@Header("Cache-Control", "private, max-age=300") @Header("Cache-Control", "private, max-age=300")
@ZodResponse({
description: "Get domestic call history",
type: SimDomesticCallHistoryResponseDto,
})
async getDomesticCallHistory( async getDomesticCallHistory(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Query() query: SimHistoryQueryDto @Query() query: SimHistoryQueryDto
): Promise<ApiSuccessResponse<SimDomesticCallHistoryResponse>> { ): Promise<SimDomesticCallHistoryResponse> {
const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const parsedQuery = simHistoryQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.getDomesticCallHistory( const result = await this.simCallHistoryService.getDomesticCallHistory(
req.user.id, req.user.id,
subscriptionId, params.id,
parsedQuery.month, parsedQuery.month,
parsedQuery.page, parsedQuery.page,
parsedQuery.limit parsedQuery.limit
); );
return { success: true, data: result }; return result;
} }
/** /**
@ -498,20 +535,24 @@ export class SubscriptionsController {
*/ */
@Get(":id/sim/call-history/international") @Get(":id/sim/call-history/international")
@Header("Cache-Control", "private, max-age=300") @Header("Cache-Control", "private, max-age=300")
@ZodResponse({
description: "Get international call history",
type: SimInternationalCallHistoryResponseDto,
})
async getInternationalCallHistory( async getInternationalCallHistory(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Query() query: SimHistoryQueryDto @Query() query: SimHistoryQueryDto
): Promise<ApiSuccessResponse<SimInternationalCallHistoryResponse>> { ): Promise<SimInternationalCallHistoryResponse> {
const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const parsedQuery = simHistoryQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.getInternationalCallHistory( const result = await this.simCallHistoryService.getInternationalCallHistory(
req.user.id, req.user.id,
subscriptionId, params.id,
parsedQuery.month, parsedQuery.month,
parsedQuery.page, parsedQuery.page,
parsedQuery.limit parsedQuery.limit
); );
return { success: true, data: result }; return result;
} }
/** /**
@ -519,19 +560,20 @@ export class SubscriptionsController {
*/ */
@Get(":id/sim/sms-history") @Get(":id/sim/sms-history")
@Header("Cache-Control", "private, max-age=300") @Header("Cache-Control", "private, max-age=300")
@ZodResponse({ description: "Get SMS history", type: SimSmsHistoryResponseDto })
async getSmsHistory( async getSmsHistory(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number, @Param() params: SubscriptionIdParamDto,
@Query() query: SimHistoryQueryDto @Query() query: SimHistoryQueryDto
): Promise<ApiSuccessResponse<SimSmsHistoryResponse>> { ): Promise<SimSmsHistoryResponse> {
const parsedQuery = simHistoryQuerySchema.parse(query as unknown); const parsedQuery = simHistoryQuerySchema.parse(query as unknown);
const result = await this.simCallHistoryService.getSmsHistory( const result = await this.simCallHistoryService.getSmsHistory(
req.user.id, req.user.id,
subscriptionId, params.id,
parsedQuery.month, parsedQuery.month,
parsedQuery.page, parsedQuery.page,
parsedQuery.limit parsedQuery.limit
); );
return { success: true, data: result }; return result;
} }
} }

View File

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

View File

@ -49,6 +49,15 @@ export const invoiceSchema = z.object({
daysOverdue: z.number().int().nonnegative().optional(), 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 // Invoice Pagination Schema
export const invoicePaginationSchema = z.object({ export const invoicePaginationSchema = z.object({
page: z.number().int().nonnegative(), page: z.number().int().nonnegative(),

View File

@ -20,6 +20,7 @@ export {
notificationListResponseSchema, notificationListResponseSchema,
notificationUnreadCountResponseSchema, notificationUnreadCountResponseSchema,
notificationQuerySchema, notificationQuerySchema,
notificationIdParamSchema,
// Types // Types
type Notification, type Notification,
type CreateNotificationRequest, type CreateNotificationRequest,
@ -27,4 +28,5 @@ export {
type NotificationListResponse, type NotificationListResponse,
type NotificationUnreadCountResponse, type NotificationUnreadCountResponse,
type NotificationQuery, type NotificationQuery,
type NotificationIdParam,
} from "./schema.js"; } from "./schema.js";

View File

@ -224,3 +224,12 @@ export const notificationQuerySchema = z.object({
}); });
export type NotificationQuery = z.infer<typeof notificationQuerySchema>; 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, totals: checkoutTotalsSchema,
}); });
export const checkoutSessionResponseSchema = apiSuccessResponseSchema( export const checkoutSessionDataSchema = z.object({
z.object({ sessionId: z.string().uuid(),
sessionId: z.string().uuid(), expiresAt: z.string(),
expiresAt: z.string(), orderType: z.enum(["Internet", "SIM", "VPN", "Other"]),
orderType: z.enum(["Internet", "SIM", "VPN", "Other"]), cart: checkoutCartSummarySchema,
cart: checkoutCartSummarySchema, });
})
);
export const checkoutSessionResponseSchema = apiSuccessResponseSchema(checkoutSessionDataSchema);
export const checkoutValidateCartDataSchema = z.object({ valid: z.boolean() });
export const checkoutValidateCartResponseSchema = apiSuccessResponseSchema( export const checkoutValidateCartResponseSchema = apiSuccessResponseSchema(
z.object({ valid: z.boolean() }) checkoutValidateCartDataSchema
); );
/** /**

View File

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

View File

@ -121,6 +121,10 @@ export const internetEligibilityRequestSchema = z.object({
address: addressSchema.partial().optional(), address: addressSchema.partial().optional(),
}); });
export const internetEligibilityRequestResponseSchema = z.object({
requestId: z.string(),
});
// ============================================================================ // ============================================================================
// SIM Product Schemas // SIM Product Schemas
// ============================================================================ // ============================================================================
@ -195,6 +199,9 @@ export type InternetCatalogCollection = z.infer<typeof internetCatalogCollection
export type InternetEligibilityStatus = z.infer<typeof internetEligibilityStatusSchema>; export type InternetEligibilityStatus = z.infer<typeof internetEligibilityStatusSchema>;
export type InternetEligibilityDetails = z.infer<typeof internetEligibilityDetailsSchema>; export type InternetEligibilityDetails = z.infer<typeof internetEligibilityDetailsSchema>;
export type InternetEligibilityRequest = z.infer<typeof internetEligibilityRequestSchema>; export type InternetEligibilityRequest = z.infer<typeof internetEligibilityRequestSchema>;
export type InternetEligibilityRequestResponse = z.infer<
typeof internetEligibilityRequestResponseSchema
>;
// SIM products // SIM products
export type SimCatalogProduct = z.infer<typeof simCatalogProductSchema>; export type SimCatalogProduct = z.infer<typeof simCatalogProductSchema>;

View File

@ -38,6 +38,7 @@ export type {
SimInfo, SimInfo,
// Portal-facing DTOs // Portal-facing DTOs
SimAvailablePlan, SimAvailablePlan,
SimAvailablePlanArray,
SimCancellationMonth, SimCancellationMonth,
SimCancellationPreview, SimCancellationPreview,
SimReissueFullRequest, SimReissueFullRequest,
@ -52,6 +53,7 @@ export type {
SimHistoryAvailableMonths, SimHistoryAvailableMonths,
SimCallHistoryImportResult, SimCallHistoryImportResult,
SimSftpFiles, SimSftpFiles,
SimSftpListResult,
// Request types // Request types
SimTopUpRequest, SimTopUpRequest,
SimPlanChangeRequest, SimPlanChangeRequest,

View File

@ -176,6 +176,9 @@ export const simAvailablePlanSchema = simCatalogProductSchema.extend({
export type SimAvailablePlan = z.infer<typeof simAvailablePlanSchema>; 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 * 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 const simSftpFilesSchema = z.array(z.string());
export type SimSftpFiles = z.infer<typeof simSftpFilesSchema>; 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 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"); 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, SubscriptionStatus,
SubscriptionCycle, SubscriptionCycle,
Subscription, Subscription,
SubscriptionArray,
SubscriptionList, SubscriptionList,
SubscriptionIdParam,
SubscriptionQueryParams, SubscriptionQueryParams,
SubscriptionQuery, SubscriptionQuery,
SubscriptionStats, SubscriptionStats,

View File

@ -51,12 +51,23 @@ export const subscriptionSchema = z.object({
serverName: z.string().optional(), serverName: z.string().optional(),
}); });
export const subscriptionArraySchema = z.array(subscriptionSchema);
// Subscription List Schema // Subscription List Schema
export const subscriptionListSchema = z.object({ export const subscriptionListSchema = z.object({
subscriptions: z.array(subscriptionSchema), subscriptions: z.array(subscriptionSchema),
totalCount: z.number().int().nonnegative(), 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 // Query Parameter Schemas
// ============================================================================ // ============================================================================
@ -116,6 +127,7 @@ export const simPlanChangeResultSchema = apiSuccessMessageResponseSchema.extend(
export type SubscriptionStatus = z.infer<typeof subscriptionStatusSchema>; export type SubscriptionStatus = z.infer<typeof subscriptionStatusSchema>;
export type SubscriptionCycle = z.infer<typeof subscriptionCycleSchema>; export type SubscriptionCycle = z.infer<typeof subscriptionCycleSchema>;
export type Subscription = z.infer<typeof subscriptionSchema>; export type Subscription = z.infer<typeof subscriptionSchema>;
export type SubscriptionArray = z.infer<typeof subscriptionArraySchema>;
export type SubscriptionList = z.infer<typeof subscriptionListSchema>; export type SubscriptionList = z.infer<typeof subscriptionListSchema>;
export type SubscriptionStats = z.infer<typeof subscriptionStatsSchema>; export type SubscriptionStats = z.infer<typeof subscriptionStatsSchema>;
export type SimActionResponse = z.infer<typeof simActionResponseSchema>; export type SimActionResponse = z.infer<typeof simActionResponseSchema>;