Refactor WHMCS HTTP Client Error Handling and Update API Response Types
- Modified error handling in WhmcsHttpClientService to avoid exposing sensitive response bodies in error messages, logging snippets only in development mode. - Updated NotificationsController and SubscriptionsController to use ApiSuccessAckResponse for success responses, enhancing type safety and consistency. - Refactored various service methods to return more specific response types, including ApiSuccessResponse for subscription-related actions and improved SIM management services. - Cleaned up unused interfaces and types in the SIM management services, streamlining the codebase and improving maintainability.
This commit is contained in:
parent
b1ff1e8fd3
commit
851207b401
@ -151,10 +151,21 @@ export class WhmcsHttpClientService {
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
const snippet = responseText?.slice(0, 300);
|
||||
throw new Error(
|
||||
`HTTP ${response.status}: ${response.statusText}${snippet ? ` | Body: ${snippet}` : ""}`
|
||||
);
|
||||
// Do NOT include response body in thrown error messages (could contain sensitive/PII and
|
||||
// would propagate into unified exception logs). If needed, emit a short snippet only in dev.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const snippet = responseText?.slice(0, 300);
|
||||
if (snippet) {
|
||||
this.logger.debug(`WHMCS non-OK response body snippet [${action}]`, {
|
||||
action,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
snippet,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return this.parseResponse<T>(responseText, action, params);
|
||||
|
||||
@ -20,6 +20,7 @@ import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
|
||||
import { NotificationService } from "./notifications.service.js";
|
||||
import type { NotificationListResponse } from "@customer-portal/domain/notifications";
|
||||
import type { ApiSuccessAckResponse } from "@customer-portal/domain/common";
|
||||
|
||||
@Controller("notifications")
|
||||
@UseGuards(RateLimitGuard)
|
||||
@ -63,7 +64,7 @@ export class NotificationsController {
|
||||
async markAsRead(
|
||||
@Req() req: RequestWithUser,
|
||||
@Param("id") notificationId: string
|
||||
): Promise<{ success: boolean }> {
|
||||
): Promise<ApiSuccessAckResponse> {
|
||||
await this.notificationService.markAsRead(notificationId, req.user.id);
|
||||
return { success: true };
|
||||
}
|
||||
@ -73,7 +74,7 @@ export class NotificationsController {
|
||||
*/
|
||||
@Post("read-all")
|
||||
@RateLimit({ limit: 10, ttl: 60 })
|
||||
async markAllAsRead(@Req() req: RequestWithUser): Promise<{ success: boolean }> {
|
||||
async markAllAsRead(@Req() req: RequestWithUser): Promise<ApiSuccessAckResponse> {
|
||||
await this.notificationService.markAllAsRead(req.user.id);
|
||||
return { success: true };
|
||||
}
|
||||
@ -86,7 +87,7 @@ export class NotificationsController {
|
||||
async dismiss(
|
||||
@Req() req: RequestWithUser,
|
||||
@Param("id") notificationId: string
|
||||
): Promise<{ success: boolean }> {
|
||||
): Promise<ApiSuccessAckResponse> {
|
||||
await this.notificationService.dismiss(notificationId, req.user.id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ -8,12 +8,7 @@ import { SimValidationService } from "./sim-validation.service.js";
|
||||
import { SimNotificationService } from "./sim-notification.service.js";
|
||||
import { SimApiNotificationService } from "./sim-api-notification.service.js";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import type { SimReissueRequest } from "@customer-portal/domain/sim";
|
||||
|
||||
export interface ReissueSimRequest {
|
||||
simType: "physical" | "esim";
|
||||
newEid?: string;
|
||||
}
|
||||
import type { SimReissueRequest, SimReissueFullRequest } from "@customer-portal/domain/sim";
|
||||
|
||||
@Injectable()
|
||||
export class EsimManagementService {
|
||||
@ -99,7 +94,7 @@ export class EsimManagementService {
|
||||
async reissueSim(
|
||||
userId: string,
|
||||
subscriptionId: number,
|
||||
request: ReissueSimRequest
|
||||
request: SimReissueFullRequest
|
||||
): Promise<void> {
|
||||
const { account } = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
const simDetails = await this.freebitService.getSimDetails(account);
|
||||
|
||||
@ -3,6 +3,11 @@ import { Logger } from "nestjs-pino";
|
||||
import { PrismaService } from "@bff/infra/database/prisma.service.js";
|
||||
import { SftpClientService } from "@bff/integrations/sftp/sftp-client.service.js";
|
||||
import { SimValidationService } from "./sim-validation.service.js";
|
||||
import type {
|
||||
SimDomesticCallHistoryResponse,
|
||||
SimInternationalCallHistoryResponse,
|
||||
SimSmsHistoryResponse,
|
||||
} from "@customer-portal/domain/sim";
|
||||
|
||||
// SmsType enum to match Prisma schema
|
||||
type SmsType = "DOMESTIC" | "INTERNATIONAL";
|
||||
@ -46,45 +51,6 @@ export interface CallHistoryPagination {
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface DomesticCallHistoryResponse {
|
||||
calls: Array<{
|
||||
id: string;
|
||||
date: string;
|
||||
time: string;
|
||||
calledTo: string;
|
||||
callLength: string; // Formatted as "Xh Xm Xs"
|
||||
callCharge: number;
|
||||
}>;
|
||||
pagination: CallHistoryPagination;
|
||||
month: string;
|
||||
}
|
||||
|
||||
export interface InternationalCallHistoryResponse {
|
||||
calls: Array<{
|
||||
id: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
stopTime: string | null;
|
||||
country: string | null;
|
||||
calledTo: string;
|
||||
callCharge: number;
|
||||
}>;
|
||||
pagination: CallHistoryPagination;
|
||||
month: string;
|
||||
}
|
||||
|
||||
export interface SmsHistoryResponse {
|
||||
messages: Array<{
|
||||
id: string;
|
||||
date: string;
|
||||
time: string;
|
||||
sentTo: string;
|
||||
type: string;
|
||||
}>;
|
||||
pagination: CallHistoryPagination;
|
||||
month: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SimCallHistoryService {
|
||||
constructor(
|
||||
@ -410,7 +376,7 @@ export class SimCallHistoryService {
|
||||
month?: string,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<DomesticCallHistoryResponse> {
|
||||
): Promise<SimDomesticCallHistoryResponse> {
|
||||
// Validate subscription ownership
|
||||
await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
// Dev/testing mode: call history data is currently sourced from a fixed account.
|
||||
@ -475,7 +441,7 @@ export class SimCallHistoryService {
|
||||
month?: string,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<InternationalCallHistoryResponse> {
|
||||
): Promise<SimInternationalCallHistoryResponse> {
|
||||
// Validate subscription ownership
|
||||
await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
// Dev/testing mode: call history data is currently sourced from a fixed account.
|
||||
@ -542,7 +508,7 @@ export class SimCallHistoryService {
|
||||
month?: string,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<SmsHistoryResponse> {
|
||||
): Promise<SimSmsHistoryResponse> {
|
||||
// Validate subscription ownership
|
||||
await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
// Dev/testing mode: call history data is currently sourced from a fixed account.
|
||||
|
||||
@ -5,31 +5,18 @@ import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/f
|
||||
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service.js";
|
||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||
import { SimValidationService } from "./sim-validation.service.js";
|
||||
import type { SimCancelRequest, SimCancelFullRequest } from "@customer-portal/domain/sim";
|
||||
import type {
|
||||
SimCancelRequest,
|
||||
SimCancelFullRequest,
|
||||
SimCancellationMonth,
|
||||
SimCancellationPreview,
|
||||
} from "@customer-portal/domain/sim";
|
||||
import { SimScheduleService } from "./sim-schedule.service.js";
|
||||
import { SimActionRunnerService } from "./sim-action-runner.service.js";
|
||||
import { SimApiNotificationService } from "./sim-api-notification.service.js";
|
||||
import { NotificationService } from "@bff/modules/notifications/notifications.service.js";
|
||||
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
|
||||
|
||||
export interface CancellationMonth {
|
||||
value: string; // YYYY-MM format
|
||||
label: string; // Display label like "November 2025"
|
||||
runDate: string; // YYYYMMDD format for API (1st of next month)
|
||||
}
|
||||
|
||||
export interface CancellationPreview {
|
||||
simNumber: string;
|
||||
serialNumber?: string;
|
||||
planCode: string;
|
||||
startDate?: string;
|
||||
minimumContractEndDate?: string;
|
||||
isWithinMinimumTerm: boolean;
|
||||
availableMonths: CancellationMonth[];
|
||||
customerEmail: string;
|
||||
customerName: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SimCancellationService {
|
||||
constructor(
|
||||
@ -52,8 +39,8 @@ export class SimCancellationService {
|
||||
/**
|
||||
* Generate available cancellation months (next 12 months)
|
||||
*/
|
||||
private generateCancellationMonths(): CancellationMonth[] {
|
||||
const months: CancellationMonth[] = [];
|
||||
private generateCancellationMonths(): SimCancellationMonth[] {
|
||||
const months: SimCancellationMonth[] = [];
|
||||
const today = new Date();
|
||||
const dayOfMonth = today.getDate();
|
||||
|
||||
@ -108,7 +95,7 @@ export class SimCancellationService {
|
||||
async getCancellationPreview(
|
||||
userId: string,
|
||||
subscriptionId: number
|
||||
): Promise<CancellationPreview> {
|
||||
): Promise<SimCancellationPreview> {
|
||||
const validation = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
const simDetails = await this.freebitService.getSimDetails(validation.account);
|
||||
|
||||
|
||||
@ -7,13 +7,13 @@ import type {
|
||||
SimPlanChangeRequest,
|
||||
SimFeaturesUpdateRequest,
|
||||
SimChangePlanFullRequest,
|
||||
SimAvailablePlan,
|
||||
} from "@customer-portal/domain/sim";
|
||||
import { SimScheduleService } from "./sim-schedule.service.js";
|
||||
import { SimActionRunnerService } from "./sim-action-runner.service.js";
|
||||
import { SimManagementQueueService } from "../queue/sim-management.queue.js";
|
||||
import { SimApiNotificationService } from "./sim-api-notification.service.js";
|
||||
import { SimServicesService } from "@bff/modules/services/services/sim-services.service.js";
|
||||
import type { SimCatalogProduct } from "@customer-portal/domain/services";
|
||||
|
||||
// Mapping from Salesforce SKU to Freebit plan code
|
||||
const SKU_TO_FREEBIT_PLAN_CODE: Record<string, string> = {
|
||||
@ -33,11 +33,6 @@ const FREEBIT_PLAN_CODE_TO_SKU: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(SKU_TO_FREEBIT_PLAN_CODE).map(([sku, code]) => [code, sku])
|
||||
);
|
||||
|
||||
export interface AvailablePlan extends SimCatalogProduct {
|
||||
freebitPlanCode: string;
|
||||
isCurrentPlan: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SimPlanService {
|
||||
constructor(
|
||||
@ -60,7 +55,7 @@ export class SimPlanService {
|
||||
* Get available plans for plan change
|
||||
* Filters by current plan type (e.g., only show DataSmsVoice plans if current is DataSmsVoice)
|
||||
*/
|
||||
async getAvailablePlans(userId: string, subscriptionId: number): Promise<AvailablePlan[]> {
|
||||
async getAvailablePlans(userId: string, subscriptionId: number): Promise<SimAvailablePlan[]> {
|
||||
const validation = await this.simValidation.validateSimSubscription(userId, subscriptionId);
|
||||
const simDetails = await this.freebitService.getSimDetails(validation.account);
|
||||
const currentPlanCode = simDetails.planCode;
|
||||
@ -81,7 +76,7 @@ export class SimPlanService {
|
||||
? allPlans.filter(p => p.simPlanType === currentPlanType)
|
||||
: allPlans.filter(p => !p.simHasFamilyDiscount); // Default: non-family plans
|
||||
|
||||
// Map to AvailablePlan with Freebit codes
|
||||
// Map to SimAvailablePlan with Freebit codes
|
||||
return filteredPlans.map(plan => {
|
||||
const freebitPlanCode = SKU_TO_FREEBIT_PLAN_CODE[plan.sku] || plan.sku;
|
||||
return {
|
||||
|
||||
@ -27,6 +27,7 @@ import type {
|
||||
SubscriptionQuery,
|
||||
} from "@customer-portal/domain/subscriptions";
|
||||
import type { InvoiceList } from "@customer-portal/domain/billing";
|
||||
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
|
||||
import { createPaginationSchema } from "@customer-portal/domain/toolkit/validation/helpers";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
@ -36,22 +37,26 @@ import {
|
||||
simFeaturesRequestSchema,
|
||||
simCancelFullRequestSchema,
|
||||
simChangePlanFullRequestSchema,
|
||||
simReissueFullRequestSchema,
|
||||
type SimTopupRequest,
|
||||
type SimChangePlanRequest,
|
||||
type SimCancelRequest,
|
||||
type SimFeaturesRequest,
|
||||
type SimCancelFullRequest,
|
||||
type SimChangePlanFullRequest,
|
||||
type SimAvailablePlan,
|
||||
type SimCancellationPreview,
|
||||
type SimDomesticCallHistoryResponse,
|
||||
type SimInternationalCallHistoryResponse,
|
||||
type SimSmsHistoryResponse,
|
||||
type SimReissueFullRequest,
|
||||
} from "@customer-portal/domain/sim";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
|
||||
import { SimPlanService } from "./sim-management/services/sim-plan.service.js";
|
||||
import { SimCancellationService } from "./sim-management/services/sim-cancellation.service.js";
|
||||
import { AdminGuard } from "@bff/core/security/guards/admin.guard.js";
|
||||
import {
|
||||
EsimManagementService,
|
||||
type ReissueSimRequest,
|
||||
} from "./sim-management/services/esim-management.service.js";
|
||||
import { EsimManagementService } from "./sim-management/services/esim-management.service.js";
|
||||
import { SimCallHistoryService } from "./sim-management/services/sim-call-history.service.js";
|
||||
import { InternetCancellationService } from "./internet-management/services/internet-cancellation.service.js";
|
||||
import {
|
||||
@ -313,7 +318,7 @@ export class SubscriptionsController {
|
||||
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 };
|
||||
}
|
||||
@ -344,7 +349,7 @@ export class SubscriptionsController {
|
||||
async getCancellationPreview(
|
||||
@Request() req: RequestWithUser,
|
||||
@Param("id", ParseIntPipe) subscriptionId: number
|
||||
) {
|
||||
): Promise<ApiSuccessResponse<SimCancellationPreview>> {
|
||||
const preview = await this.simCancellationService.getCancellationPreview(
|
||||
req.user.id,
|
||||
subscriptionId
|
||||
@ -373,10 +378,11 @@ export class SubscriptionsController {
|
||||
* Reissue SIM (both eSIM and physical SIM)
|
||||
*/
|
||||
@Post(":id/sim/reissue")
|
||||
@UsePipes(new ZodValidationPipe(simReissueFullRequestSchema))
|
||||
async reissueSim(
|
||||
@Request() req: RequestWithUser,
|
||||
@Param("id", ParseIntPipe) subscriptionId: number,
|
||||
@Body() body: ReissueSimRequest
|
||||
@Body() body: SimReissueFullRequest
|
||||
): Promise<SimActionResponse> {
|
||||
await this.esimManagementService.reissueSim(req.user.id, subscriptionId, body);
|
||||
|
||||
@ -438,7 +444,7 @@ export class SubscriptionsController {
|
||||
@Query("month") month?: string,
|
||||
@Query("page") page?: string,
|
||||
@Query("limit") limit?: string
|
||||
) {
|
||||
): Promise<ApiSuccessResponse<SimDomesticCallHistoryResponse>> {
|
||||
const pageNum = parseInt(page || "1", 10);
|
||||
const limitNum = parseInt(limit || "50", 10);
|
||||
|
||||
@ -470,7 +476,7 @@ export class SubscriptionsController {
|
||||
@Query("month") month?: string,
|
||||
@Query("page") page?: string,
|
||||
@Query("limit") limit?: string
|
||||
) {
|
||||
): Promise<ApiSuccessResponse<SimInternationalCallHistoryResponse>> {
|
||||
const pageNum = parseInt(page || "1", 10);
|
||||
const limitNum = parseInt(limit || "50", 10);
|
||||
|
||||
@ -502,7 +508,7 @@ export class SubscriptionsController {
|
||||
@Query("month") month?: string,
|
||||
@Query("page") page?: string,
|
||||
@Query("limit") limit?: string
|
||||
) {
|
||||
): Promise<ApiSuccessResponse<SimSmsHistoryResponse>> {
|
||||
const pageNum = parseInt(page || "1", 10);
|
||||
const limitNum = parseInt(limit || "50", 10);
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import {
|
||||
} from "@customer-portal/domain/support";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
|
||||
import { hashEmailForLogs } from "./support.logging.js";
|
||||
import type { ApiSuccessMessageResponse } from "@customer-portal/domain/common";
|
||||
|
||||
@Controller("support")
|
||||
export class SupportController {
|
||||
@ -74,7 +75,7 @@ export class SupportController {
|
||||
async publicContact(
|
||||
@Body(new ZodValidationPipe(publicContactRequestSchema))
|
||||
body: PublicContactRequest
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
): Promise<ApiSuccessMessageResponse> {
|
||||
this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) });
|
||||
|
||||
try {
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { apiClient, getDataOrThrow } from "@/lib/api";
|
||||
import type { ApiSuccessAckResponse } from "@customer-portal/domain/common";
|
||||
import type { NotificationListResponse } from "@customer-portal/domain/notifications";
|
||||
|
||||
const BASE_PATH = "/api/notifications";
|
||||
@ -42,20 +43,20 @@ export const notificationService = {
|
||||
* Mark a notification as read
|
||||
*/
|
||||
async markAsRead(notificationId: string): Promise<void> {
|
||||
await apiClient.POST<{ success: boolean }>(`${BASE_PATH}/${notificationId}/read`);
|
||||
await apiClient.POST<ApiSuccessAckResponse>(`${BASE_PATH}/${notificationId}/read`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark all notifications as read
|
||||
*/
|
||||
async markAllAsRead(): Promise<void> {
|
||||
await apiClient.POST<{ success: boolean }>(`${BASE_PATH}/read-all`);
|
||||
await apiClient.POST<ApiSuccessAckResponse>(`${BASE_PATH}/read-all`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Dismiss a notification
|
||||
*/
|
||||
async dismiss(notificationId: string): Promise<void> {
|
||||
await apiClient.POST<{ success: boolean }>(`${BASE_PATH}/${notificationId}/dismiss`);
|
||||
await apiClient.POST<ApiSuccessAckResponse>(`${BASE_PATH}/${notificationId}/dismiss`);
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,43 +1,28 @@
|
||||
import { apiClient } from "@/lib/api";
|
||||
import { apiClient, getDataOrThrow } from "@/lib/api";
|
||||
import type {
|
||||
InternetCancellationPreview,
|
||||
InternetCancelRequest,
|
||||
InternetCancellationPreview,
|
||||
} from "@customer-portal/domain/subscriptions";
|
||||
|
||||
export interface InternetCancellationMonth {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface InternetCancellationPreviewResponse {
|
||||
productName: string;
|
||||
billingAmount: number;
|
||||
nextDueDate?: string;
|
||||
registrationDate?: string;
|
||||
availableMonths: InternetCancellationMonth[];
|
||||
customerEmail: string;
|
||||
customerName: string;
|
||||
}
|
||||
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
|
||||
|
||||
export const internetActionsService = {
|
||||
/**
|
||||
* Get cancellation preview (available months, service details)
|
||||
*/
|
||||
async getCancellationPreview(
|
||||
subscriptionId: string
|
||||
): Promise<InternetCancellationPreviewResponse> {
|
||||
const response = await apiClient.GET<{
|
||||
success: boolean;
|
||||
data: InternetCancellationPreview;
|
||||
}>("/api/subscriptions/{subscriptionId}/internet/cancellation-preview", {
|
||||
params: { path: { subscriptionId } },
|
||||
});
|
||||
async getCancellationPreview(subscriptionId: string): Promise<InternetCancellationPreview> {
|
||||
const response = await apiClient.GET<ApiSuccessResponse<InternetCancellationPreview>>(
|
||||
"/api/subscriptions/{subscriptionId}/internet/cancellation-preview",
|
||||
{
|
||||
params: { path: { subscriptionId } },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.data?.data) {
|
||||
const payload = getDataOrThrow(response, "Failed to load cancellation information");
|
||||
if (!payload.data) {
|
||||
throw new Error("Failed to load cancellation information");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
return payload.data;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -1,15 +1,23 @@
|
||||
import { apiClient } from "@/lib/api";
|
||||
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
|
||||
import {
|
||||
simInfoSchema,
|
||||
type SimAvailablePlan,
|
||||
type SimInfo,
|
||||
type SimCancelFullRequest,
|
||||
type SimChangePlanFullRequest,
|
||||
type SimCancellationPreview,
|
||||
type SimDomesticCallHistoryResponse,
|
||||
type SimInternationalCallHistoryResponse,
|
||||
type SimSmsHistoryResponse,
|
||||
type SimReissueFullRequest,
|
||||
} from "@customer-portal/domain/sim";
|
||||
import type {
|
||||
SimTopUpRequest,
|
||||
SimPlanChangeRequest,
|
||||
SimCancelRequest,
|
||||
} from "@customer-portal/domain/sim";
|
||||
import type { SimPlanChangeResult } from "@customer-portal/domain/subscriptions";
|
||||
|
||||
// Types imported from domain - no duplication
|
||||
// Domain schemas provide validation rules:
|
||||
@ -17,41 +25,6 @@ import type {
|
||||
// - SimPlanChangeRequest: newPlanCode, assignGlobalIp, scheduledAt (YYYYMMDD format)
|
||||
// - SimCancelRequest: scheduledAt (YYYYMMDD format)
|
||||
|
||||
export interface AvailablePlan {
|
||||
id: string;
|
||||
name: string;
|
||||
sku: string;
|
||||
description?: string;
|
||||
monthlyPrice?: number;
|
||||
simDataSize?: string;
|
||||
simPlanType?: string;
|
||||
freebitPlanCode: string;
|
||||
isCurrentPlan: boolean;
|
||||
}
|
||||
|
||||
export interface CancellationMonth {
|
||||
value: string;
|
||||
label: string;
|
||||
runDate: string;
|
||||
}
|
||||
|
||||
export interface CancellationPreview {
|
||||
simNumber: string;
|
||||
serialNumber?: string;
|
||||
planCode: string;
|
||||
startDate?: string;
|
||||
minimumContractEndDate?: string;
|
||||
isWithinMinimumTerm: boolean;
|
||||
availableMonths: CancellationMonth[];
|
||||
customerEmail: string;
|
||||
customerName: string;
|
||||
}
|
||||
|
||||
export interface ReissueSimRequest {
|
||||
simType: "physical" | "esim";
|
||||
newEid?: string;
|
||||
}
|
||||
|
||||
export const simActionsService = {
|
||||
async topUp(subscriptionId: string, request: SimTopUpRequest): Promise<void> {
|
||||
await apiClient.POST("/api/subscriptions/{subscriptionId}/sim/top-up", {
|
||||
@ -71,14 +44,13 @@ export const simActionsService = {
|
||||
subscriptionId: string,
|
||||
request: SimChangePlanFullRequest
|
||||
): Promise<{ scheduledAt?: string }> {
|
||||
const response = await apiClient.POST<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
scheduledAt?: string;
|
||||
}>("/api/subscriptions/{subscriptionId}/sim/change-plan-full", {
|
||||
params: { path: { subscriptionId } },
|
||||
body: request,
|
||||
});
|
||||
const response = await apiClient.POST<SimPlanChangeResult>(
|
||||
"/api/subscriptions/{subscriptionId}/sim/change-plan-full",
|
||||
{
|
||||
params: { path: { subscriptionId } },
|
||||
body: request,
|
||||
}
|
||||
);
|
||||
return { scheduledAt: response.data?.scheduledAt };
|
||||
},
|
||||
|
||||
@ -108,27 +80,27 @@ export const simActionsService = {
|
||||
return simInfoSchema.parse(response.data);
|
||||
},
|
||||
|
||||
async getAvailablePlans(subscriptionId: string): Promise<AvailablePlan[]> {
|
||||
const response = await apiClient.GET<{ success: boolean; data: AvailablePlan[] }>(
|
||||
async getAvailablePlans(subscriptionId: string): Promise<SimAvailablePlan[]> {
|
||||
const response = await apiClient.GET<ApiSuccessResponse<SimAvailablePlan[]>>(
|
||||
"/api/subscriptions/{subscriptionId}/sim/available-plans",
|
||||
{
|
||||
params: { path: { subscriptionId } },
|
||||
}
|
||||
);
|
||||
return response.data?.data || [];
|
||||
return response.data?.data ?? [];
|
||||
},
|
||||
|
||||
async getCancellationPreview(subscriptionId: string): Promise<CancellationPreview | null> {
|
||||
const response = await apiClient.GET<{ success: boolean; data: CancellationPreview }>(
|
||||
async getCancellationPreview(subscriptionId: string): Promise<SimCancellationPreview | null> {
|
||||
const response = await apiClient.GET<ApiSuccessResponse<SimCancellationPreview>>(
|
||||
"/api/subscriptions/{subscriptionId}/sim/cancellation-preview",
|
||||
{
|
||||
params: { path: { subscriptionId } },
|
||||
}
|
||||
);
|
||||
return response.data?.data || null;
|
||||
return response.data?.data ?? null;
|
||||
},
|
||||
|
||||
async reissueSim(subscriptionId: string, request: ReissueSimRequest): Promise<void> {
|
||||
async reissueSim(subscriptionId: string, request: SimReissueFullRequest): Promise<void> {
|
||||
await apiClient.POST("/api/subscriptions/{subscriptionId}/sim/reissue", {
|
||||
params: { path: { subscriptionId } },
|
||||
body: request,
|
||||
@ -142,19 +114,19 @@ export const simActionsService = {
|
||||
month?: string,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<DomesticCallHistoryResponse | null> {
|
||||
): Promise<SimDomesticCallHistoryResponse | null> {
|
||||
const params: Record<string, string> = {};
|
||||
if (month) params.month = month;
|
||||
params.page = String(page);
|
||||
params.limit = String(limit);
|
||||
|
||||
const response = await apiClient.GET<{ success: boolean; data: DomesticCallHistoryResponse }>(
|
||||
const response = await apiClient.GET<ApiSuccessResponse<SimDomesticCallHistoryResponse>>(
|
||||
"/api/subscriptions/{subscriptionId}/sim/call-history/domestic",
|
||||
{
|
||||
params: { path: { subscriptionId }, query: params },
|
||||
}
|
||||
);
|
||||
return response.data?.data || null;
|
||||
return response.data?.data ?? null;
|
||||
},
|
||||
|
||||
async getInternationalCallHistory(
|
||||
@ -162,19 +134,19 @@ export const simActionsService = {
|
||||
month?: string,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<InternationalCallHistoryResponse | null> {
|
||||
): Promise<SimInternationalCallHistoryResponse | null> {
|
||||
const params: Record<string, string> = {};
|
||||
if (month) params.month = month;
|
||||
params.page = String(page);
|
||||
params.limit = String(limit);
|
||||
|
||||
const response = await apiClient.GET<{
|
||||
success: boolean;
|
||||
data: InternationalCallHistoryResponse;
|
||||
}>("/api/subscriptions/{subscriptionId}/sim/call-history/international", {
|
||||
params: { path: { subscriptionId }, query: params },
|
||||
});
|
||||
return response.data?.data || null;
|
||||
const response = await apiClient.GET<ApiSuccessResponse<SimInternationalCallHistoryResponse>>(
|
||||
"/api/subscriptions/{subscriptionId}/sim/call-history/international",
|
||||
{
|
||||
params: { path: { subscriptionId }, query: params },
|
||||
}
|
||||
);
|
||||
return response.data?.data ?? null;
|
||||
},
|
||||
|
||||
async getSmsHistory(
|
||||
@ -182,79 +154,26 @@ export const simActionsService = {
|
||||
month?: string,
|
||||
page: number = 1,
|
||||
limit: number = 50
|
||||
): Promise<SmsHistoryResponse | null> {
|
||||
): Promise<SimSmsHistoryResponse | null> {
|
||||
const params: Record<string, string> = {};
|
||||
if (month) params.month = month;
|
||||
params.page = String(page);
|
||||
params.limit = String(limit);
|
||||
|
||||
const response = await apiClient.GET<{ success: boolean; data: SmsHistoryResponse }>(
|
||||
const response = await apiClient.GET<ApiSuccessResponse<SimSmsHistoryResponse>>(
|
||||
"/api/subscriptions/{subscriptionId}/sim/sms-history",
|
||||
{
|
||||
params: { path: { subscriptionId }, query: params },
|
||||
}
|
||||
);
|
||||
return response.data?.data || null;
|
||||
return response.data?.data ?? null;
|
||||
},
|
||||
|
||||
async getAvailableHistoryMonths(): Promise<string[]> {
|
||||
const response = await apiClient.GET<{ success: boolean; data: string[] }>(
|
||||
const response = await apiClient.GET<ApiSuccessResponse<string[]>>(
|
||||
"/api/subscriptions/sim/call-history/available-months",
|
||||
{}
|
||||
);
|
||||
return response.data?.data || [];
|
||||
return response.data?.data ?? [];
|
||||
},
|
||||
};
|
||||
|
||||
// Additional types for call/SMS history
|
||||
export interface CallHistoryPagination {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface DomesticCallRecord {
|
||||
id: string;
|
||||
date: string;
|
||||
time: string;
|
||||
calledTo: string;
|
||||
callLength: string;
|
||||
callCharge: number;
|
||||
}
|
||||
|
||||
export interface DomesticCallHistoryResponse {
|
||||
calls: DomesticCallRecord[];
|
||||
pagination: CallHistoryPagination;
|
||||
month: string;
|
||||
}
|
||||
|
||||
export interface InternationalCallRecord {
|
||||
id: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
stopTime: string | null;
|
||||
country: string | null;
|
||||
calledTo: string;
|
||||
callCharge: number;
|
||||
}
|
||||
|
||||
export interface InternationalCallHistoryResponse {
|
||||
calls: InternationalCallRecord[];
|
||||
pagination: CallHistoryPagination;
|
||||
month: string;
|
||||
}
|
||||
|
||||
export interface SmsRecord {
|
||||
id: string;
|
||||
date: string;
|
||||
time: string;
|
||||
sentTo: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SmsHistoryResponse {
|
||||
messages: SmsRecord[];
|
||||
pagination: CallHistoryPagination;
|
||||
month: string;
|
||||
}
|
||||
|
||||
@ -3,10 +3,8 @@
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import {
|
||||
internetActionsService,
|
||||
type InternetCancellationPreviewResponse,
|
||||
} from "@/features/subscriptions/services/internet-actions.service";
|
||||
import { internetActionsService } from "@/features/subscriptions/services/internet-actions.service";
|
||||
import type { InternetCancellationPreview } from "@customer-portal/domain/subscriptions";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
@ -40,7 +38,7 @@ export function InternetCancelContainer() {
|
||||
|
||||
const [step, setStep] = useState<Step>(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [preview, setPreview] = useState<InternetCancellationPreviewResponse | null>(null);
|
||||
const [preview, setPreview] = useState<InternetCancellationPreview | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
|
||||
@ -6,12 +6,12 @@ import { useParams } from "next/navigation";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { PhoneIcon, GlobeAltIcon, ChatBubbleLeftIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
simActionsService,
|
||||
type DomesticCallHistoryResponse,
|
||||
type InternationalCallHistoryResponse,
|
||||
type SmsHistoryResponse,
|
||||
} from "@/features/subscriptions/services/sim-actions.service";
|
||||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||
import type {
|
||||
SimDomesticCallHistoryResponse,
|
||||
SimInternationalCallHistoryResponse,
|
||||
SimSmsHistoryResponse,
|
||||
} from "@customer-portal/domain/sim";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { Formatting } from "@customer-portal/domain/toolkit";
|
||||
|
||||
@ -64,10 +64,10 @@ export function SimCallHistoryContainer() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Data states
|
||||
const [domesticData, setDomesticData] = useState<DomesticCallHistoryResponse | null>(null);
|
||||
const [domesticData, setDomesticData] = useState<SimDomesticCallHistoryResponse | null>(null);
|
||||
const [internationalData, setInternationalData] =
|
||||
useState<InternationalCallHistoryResponse | null>(null);
|
||||
const [smsData, setSmsData] = useState<SmsHistoryResponse | null>(null);
|
||||
useState<SimInternationalCallHistoryResponse | null>(null);
|
||||
const [smsData, setSmsData] = useState<SimSmsHistoryResponse | null>(null);
|
||||
|
||||
// Pagination states
|
||||
const [domesticPage, setDomesticPage] = useState(1);
|
||||
|
||||
@ -3,10 +3,8 @@
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import {
|
||||
simActionsService,
|
||||
type CancellationPreview,
|
||||
} from "@/features/subscriptions/services/sim-actions.service";
|
||||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||
import type { SimCancellationPreview } from "@customer-portal/domain/sim";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
@ -40,7 +38,7 @@ export function SimCancelContainer() {
|
||||
|
||||
const [step, setStep] = useState<Step>(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [preview, setPreview] = useState<CancellationPreview | null>(null);
|
||||
const [preview, setPreview] = useState<SimCancellationPreview | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
|
||||
@ -6,10 +6,8 @@ import { useParams } from "next/navigation";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { DevicePhoneMobileIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
simActionsService,
|
||||
type AvailablePlan,
|
||||
} from "@/features/subscriptions/services/sim-actions.service";
|
||||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||
import type { SimAvailablePlan } from "@customer-portal/domain/sim";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import { Formatting } from "@customer-portal/domain/toolkit";
|
||||
import { Button } from "@/components/atoms";
|
||||
@ -19,8 +17,8 @@ const { formatCurrency } = Formatting;
|
||||
export function SimChangePlanContainer() {
|
||||
const params = useParams();
|
||||
const subscriptionId = params.id as string;
|
||||
const [plans, setPlans] = useState<AvailablePlan[]>([]);
|
||||
const [selectedPlan, setSelectedPlan] = useState<AvailablePlan | null>(null);
|
||||
const [plans, setPlans] = useState<SimAvailablePlan[]>([]);
|
||||
const [selectedPlan, setSelectedPlan] = useState<SimAvailablePlan | null>(null);
|
||||
const [assignGlobalIp, setAssignGlobalIp] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@ -6,10 +6,8 @@ import { useParams } from "next/navigation";
|
||||
import { PageLayout } from "@/components/templates/PageLayout";
|
||||
import { SubCard } from "@/components/molecules/SubCard/SubCard";
|
||||
import { DevicePhoneMobileIcon, DeviceTabletIcon, CpuChipIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
simActionsService,
|
||||
type ReissueSimRequest,
|
||||
} from "@/features/subscriptions/services/sim-actions.service";
|
||||
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
|
||||
import type { SimReissueFullRequest } from "@customer-portal/domain/sim";
|
||||
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
|
||||
import type { SimDetails } from "@/features/sim-management/components/SimDetailsCard";
|
||||
import { Button } from "@/components/atoms";
|
||||
@ -71,10 +69,8 @@ export function SimReissueContainer() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const request: ReissueSimRequest = {
|
||||
simType,
|
||||
...(simType === "esim" && { newEid: newEid.trim() }),
|
||||
};
|
||||
const request: SimReissueFullRequest =
|
||||
simType === "esim" ? { simType, newEid: newEid.trim() } : { simType };
|
||||
await simActionsService.reissueSim(subscriptionId, request);
|
||||
|
||||
if (simType === "esim") {
|
||||
|
||||
@ -101,6 +101,21 @@ export const apiSuccessResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T)
|
||||
data: dataSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Schema for successful API acknowledgements (no payload)
|
||||
*/
|
||||
export const apiSuccessAckResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
});
|
||||
|
||||
/**
|
||||
* Schema for successful API responses with a human-readable message (no data payload)
|
||||
*/
|
||||
export const apiSuccessMessageResponseSchema = z.object({
|
||||
success: z.literal(true),
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Schema for error API responses
|
||||
*/
|
||||
|
||||
@ -48,6 +48,23 @@ export interface ApiSuccessResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Success acknowledgement response (no payload)
|
||||
* Used for endpoints that simply confirm the operation succeeded.
|
||||
*/
|
||||
export interface ApiSuccessAckResponse {
|
||||
success: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Success message response (no data payload)
|
||||
* Used for endpoints that return a human-readable message (e.g., actions).
|
||||
*/
|
||||
export interface ApiSuccessMessageResponse {
|
||||
success: true;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
success: false;
|
||||
error: {
|
||||
|
||||
@ -36,6 +36,18 @@ export type {
|
||||
SimTopUpHistoryEntry,
|
||||
SimTopUpHistory,
|
||||
SimInfo,
|
||||
// Portal-facing DTOs
|
||||
SimAvailablePlan,
|
||||
SimCancellationMonth,
|
||||
SimCancellationPreview,
|
||||
SimReissueFullRequest,
|
||||
SimCallHistoryPagination,
|
||||
SimDomesticCallRecord,
|
||||
SimDomesticCallHistoryResponse,
|
||||
SimInternationalCallRecord,
|
||||
SimInternationalCallHistoryResponse,
|
||||
SimSmsRecord,
|
||||
SimSmsHistoryResponse,
|
||||
// Request types
|
||||
SimTopUpRequest,
|
||||
SimPlanChangeRequest,
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { simCatalogProductSchema } from "../services/schema.js";
|
||||
|
||||
export const simStatusSchema = z.enum(["active", "suspended", "cancelled", "pending"]);
|
||||
|
||||
@ -157,6 +158,154 @@ export const simReissueRequestSchema = z.object({
|
||||
simType: z.enum(["physical", "esim"]).optional(),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Portal-facing SIM DTOs (responses + request contracts)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Available plan for SIM plan change (portal-facing)
|
||||
*
|
||||
* This extends the catalog's SIM product shape with:
|
||||
* - freebitPlanCode: the actual plan code used by Freebit
|
||||
* - isCurrentPlan: whether this is the customer's current plan
|
||||
*/
|
||||
export const simAvailablePlanSchema = simCatalogProductSchema.extend({
|
||||
freebitPlanCode: z.string(),
|
||||
isCurrentPlan: z.boolean(),
|
||||
});
|
||||
|
||||
export type SimAvailablePlan = z.infer<typeof simAvailablePlanSchema>;
|
||||
|
||||
/**
|
||||
* Cancellation month option for SIM cancellation preview
|
||||
*/
|
||||
export const simCancellationMonthSchema = z.object({
|
||||
value: z.string().regex(/^\d{4}-\d{2}$/, "Month must be in YYYY-MM format"),
|
||||
label: z.string(),
|
||||
runDate: z.string().regex(/^\d{8}$/, "runDate must be in YYYYMMDD format"),
|
||||
});
|
||||
|
||||
export type SimCancellationMonth = z.infer<typeof simCancellationMonthSchema>;
|
||||
|
||||
/**
|
||||
* SIM cancellation preview payload (portal-facing)
|
||||
*/
|
||||
export const simCancellationPreviewSchema = z.object({
|
||||
simNumber: z.string(),
|
||||
serialNumber: z.string().optional(),
|
||||
planCode: z.string(),
|
||||
startDate: z.string().optional(),
|
||||
minimumContractEndDate: z.string().optional(),
|
||||
isWithinMinimumTerm: z.boolean(),
|
||||
availableMonths: z.array(simCancellationMonthSchema),
|
||||
customerEmail: z.string(),
|
||||
customerName: z.string(),
|
||||
});
|
||||
|
||||
export type SimCancellationPreview = z.infer<typeof simCancellationPreviewSchema>;
|
||||
|
||||
/**
|
||||
* Reissue SIM request (full flow) for `/api/subscriptions/:id/sim/reissue`
|
||||
*
|
||||
* - Physical SIM: simType="physical" (newEid optional/ignored)
|
||||
* - eSIM: simType="esim" and newEid is required
|
||||
*/
|
||||
export const simReissueFullRequestSchema = z
|
||||
.object({
|
||||
simType: z.enum(["physical", "esim"]),
|
||||
newEid: z
|
||||
.string()
|
||||
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.simType === "esim" && !data.newEid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "newEid is required for eSIM reissue",
|
||||
path: ["newEid"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type SimReissueFullRequest = z.infer<typeof simReissueFullRequestSchema>;
|
||||
|
||||
// ============================================================================
|
||||
// SIM Call/SMS History (portal-facing)
|
||||
// ============================================================================
|
||||
|
||||
const simHistoryMonthSchema = z.string().regex(/^\d{4}-\d{2}$/, "Month must be in YYYY-MM 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");
|
||||
|
||||
export const simCallHistoryPaginationSchema = z.object({
|
||||
page: z.number().int().min(1),
|
||||
limit: z.number().int().min(1),
|
||||
total: z.number().int().min(0),
|
||||
totalPages: z.number().int().min(0),
|
||||
});
|
||||
|
||||
export type SimCallHistoryPagination = z.infer<typeof simCallHistoryPaginationSchema>;
|
||||
|
||||
export const simDomesticCallRecordSchema = z.object({
|
||||
id: z.string(),
|
||||
date: isoDateSchema,
|
||||
time: timeHmsSchema,
|
||||
calledTo: z.string(),
|
||||
callLength: z.string(),
|
||||
callCharge: z.number(),
|
||||
});
|
||||
|
||||
export type SimDomesticCallRecord = z.infer<typeof simDomesticCallRecordSchema>;
|
||||
|
||||
export const simDomesticCallHistoryResponseSchema = z.object({
|
||||
calls: z.array(simDomesticCallRecordSchema),
|
||||
pagination: simCallHistoryPaginationSchema,
|
||||
month: simHistoryMonthSchema,
|
||||
});
|
||||
|
||||
export type SimDomesticCallHistoryResponse = z.infer<typeof simDomesticCallHistoryResponseSchema>;
|
||||
|
||||
export const simInternationalCallRecordSchema = z.object({
|
||||
id: z.string(),
|
||||
date: isoDateSchema,
|
||||
startTime: timeHmsSchema,
|
||||
stopTime: z.string().nullable(),
|
||||
country: z.string().nullable(),
|
||||
calledTo: z.string(),
|
||||
callCharge: z.number(),
|
||||
});
|
||||
|
||||
export type SimInternationalCallRecord = z.infer<typeof simInternationalCallRecordSchema>;
|
||||
|
||||
export const simInternationalCallHistoryResponseSchema = z.object({
|
||||
calls: z.array(simInternationalCallRecordSchema),
|
||||
pagination: simCallHistoryPaginationSchema,
|
||||
month: simHistoryMonthSchema,
|
||||
});
|
||||
|
||||
export type SimInternationalCallHistoryResponse = z.infer<
|
||||
typeof simInternationalCallHistoryResponseSchema
|
||||
>;
|
||||
|
||||
export const simSmsRecordSchema = z.object({
|
||||
id: z.string(),
|
||||
date: isoDateSchema,
|
||||
time: timeHmsSchema,
|
||||
sentTo: z.string(),
|
||||
type: z.string(),
|
||||
});
|
||||
|
||||
export type SimSmsRecord = z.infer<typeof simSmsRecordSchema>;
|
||||
|
||||
export const simSmsHistoryResponseSchema = z.object({
|
||||
messages: z.array(simSmsRecordSchema),
|
||||
pagination: simCallHistoryPaginationSchema,
|
||||
month: simHistoryMonthSchema,
|
||||
});
|
||||
|
||||
export type SimSmsHistoryResponse = z.infer<typeof simSmsHistoryResponseSchema>;
|
||||
|
||||
// Enhanced cancellation request with more details
|
||||
export const simCancelFullRequestSchema = z
|
||||
.object({
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { apiSuccessMessageResponseSchema } from "../common/schema.js";
|
||||
|
||||
// Subscription Status Schema
|
||||
export const subscriptionStatusSchema = z.enum([
|
||||
@ -92,20 +93,20 @@ export const subscriptionStatsSchema = z.object({
|
||||
/**
|
||||
* Schema for SIM action responses (top-up, cancellation, feature updates)
|
||||
*/
|
||||
export const simActionResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string(),
|
||||
export const simActionResponseSchema = apiSuccessMessageResponseSchema.extend({
|
||||
data: z.unknown().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Schema for SIM plan change result with IP addresses
|
||||
*/
|
||||
export const simPlanChangeResultSchema = z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string(),
|
||||
export const simPlanChangeResultSchema = apiSuccessMessageResponseSchema.extend({
|
||||
ipv4: z.string().optional(),
|
||||
ipv6: z.string().optional(),
|
||||
scheduledAt: z
|
||||
.string()
|
||||
.regex(/^\d{8}$/, "Scheduled date must be in YYYYMMDD format")
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user