barsa 1220f219e4 Add SIM top-up pricing endpoints and integrate pricing service into subscriptions module
- Introduced new endpoints in SubscriptionsController for retrieving SIM top-up pricing and previewing pricing based on quota.
- Integrated SimTopUpPricingService into the subscriptions module to handle pricing logic.
- Updated TopUpModal component to utilize the new pricing service for dynamic cost calculations and validation.
- Enhanced error handling and user feedback in the TopUpModal for improved user experience during data top-up operations.
- Refactored order display utilities to support new pricing structures and ensure consistent presentation across the application.
2025-11-18 11:14:05 +09:00

395 lines
13 KiB
TypeScript

/**
* Orders Domain - Schemas
*
* Zod schemas for runtime validation of order data.
*/
import { z } from "zod";
import { apiSuccessResponseSchema } from "../common";
// ============================================================================
// Enum Value Arrays (for Zod schemas)
// ============================================================================
// These arrays are used by Zod schemas for validation.
// For type-safe constants, see contract.ts (ACCESS_MODE, ACTIVATION_TYPE, SIM_TYPE)
const ACCESS_MODE_VALUES = ["IPoE-BYOR", "IPoE-HGW", "PPPoE"] as const;
const ACTIVATION_TYPE_VALUES = ["Immediate", "Scheduled"] as const;
const SIM_TYPE_VALUES = ["eSIM", "Physical SIM"] as const;
const PORTING_GENDER_VALUES = ["Male", "Female", "Corporate/Other"] as const;
// ============================================================================
// Order Item Summary Schema
// ============================================================================
export const orderItemSummarySchema = z.object({
productName: z.string().optional(),
name: z.string().optional(),
sku: z.string().optional(),
productId: z.string().optional(),
status: z.string().optional(),
billingCycle: z.string().optional(),
itemClass: z.string().optional(),
quantity: z.number().int().min(0).optional(),
unitPrice: z.number().optional(),
totalPrice: z.number().optional(),
isBundledAddon: z.boolean().optional(),
bundledAddonId: z.string().optional(),
});
// ============================================================================
// Order Item Details Schema
// ============================================================================
export const orderItemDetailsSchema = z.object({
id: z.string(),
orderId: z.string(),
quantity: z.number().int().min(1),
unitPrice: z.number().optional(),
totalPrice: z.number().optional(),
billingCycle: z.string().optional(),
product: z.object({
id: z.string().optional(),
name: z.string().optional(),
sku: z.string().optional(),
itemClass: z.string().optional(),
whmcsProductId: z.string().optional(),
internetOfferingType: z.string().optional(),
internetPlanTier: z.string().optional(),
vpnRegion: z.string().optional(),
isBundledAddon: z.boolean().optional(),
bundledAddonId: z.string().optional(),
}).optional(),
});
// ============================================================================
// Order Summary Schema
// ============================================================================
export const orderSummarySchema = z.object({
id: z.string(),
orderNumber: z.string(),
status: z.string(),
orderType: z.string().optional(),
effectiveDate: z.string(), // IsoDateTimeString
totalAmount: z.number().optional(),
createdDate: z.string(), // IsoDateTimeString
lastModifiedDate: z.string(), // IsoDateTimeString
whmcsOrderId: z.string().optional(),
activationStatus: z.string().optional(),
itemsSummary: z.array(orderItemSummarySchema),
});
// ============================================================================
// Order Details Schema
// ============================================================================
export const orderDetailsSchema = orderSummarySchema.extend({
accountId: z.string().optional(),
accountName: z.string().optional(),
pricebook2Id: z.string().optional(),
activationType: z.string().optional(),
activationStatus: z.string().optional(),
activationScheduledAt: z.string().optional(), // IsoDateTimeString
activationErrorCode: z.string().optional(),
activationErrorMessage: z.string().optional(),
activatedDate: z.string().optional(), // IsoDateTimeString
items: z.array(orderItemDetailsSchema),
});
// ============================================================================
// Query Parameter Schemas
// ============================================================================
/**
* Schema for order query parameters
*/
export const orderQueryParamsSchema = z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().max(100).optional(),
status: z.string().optional(),
orderType: z.string().optional(),
});
// ============================================================================
// Order Creation Schemas
// ============================================================================
const orderConfigurationsAddressSchema = z.object({
street: z.string().nullable().optional(),
streetLine2: z.string().nullable().optional(),
city: z.string().nullable().optional(),
state: z.string().nullable().optional(),
postalCode: z.string().nullable().optional(),
country: z.string().nullable().optional(),
});
export const orderConfigurationsSchema = z.object({
activationType: z.enum(ACTIVATION_TYPE_VALUES).optional(),
scheduledAt: z.string().optional(),
accessMode: z.enum(ACCESS_MODE_VALUES).optional(),
simType: z.enum(SIM_TYPE_VALUES).optional(),
eid: z.string().optional(),
isMnp: z.string().optional(),
mnpNumber: z.string().optional(),
mnpExpiry: z.string().optional(),
mnpPhone: z.string().optional(),
mvnoAccountNumber: z.string().optional(),
portingLastName: z.string().optional(),
portingFirstName: z.string().optional(),
portingLastNameKatakana: z.string().optional(),
portingFirstNameKatakana: z.string().optional(),
portingGender: z.enum(PORTING_GENDER_VALUES).optional(),
portingDateOfBirth: z.string().optional(),
address: orderConfigurationsAddressSchema.optional(),
});
/**
* Schema for raw checkout selections (typically derived from UI/query params)
*/
export const orderSelectionsSchema = z
.object({
plan: z.string().optional(),
planId: z.string().optional(),
planSku: z.string().optional(),
planIdSku: z.string().optional(),
installationSku: z.string().optional(),
activationFeeSku: z.string().optional(),
activationSku: z.string().optional(),
addonSku: z.string().optional(),
addons: z.string().optional(),
accessMode: z.enum(ACCESS_MODE_VALUES).optional(),
activationType: z.enum(ACTIVATION_TYPE_VALUES).optional(),
scheduledAt: z.string().optional(),
simType: z.enum(SIM_TYPE_VALUES).optional(),
eid: z.string().optional(),
isMnp: z.string().optional(),
mnpNumber: z.string().optional(),
mnpExpiry: z.string().optional(),
mnpPhone: z.string().optional(),
mvnoAccountNumber: z.string().optional(),
portingLastName: z.string().optional(),
portingFirstName: z.string().optional(),
portingLastNameKatakana: z.string().optional(),
portingFirstNameKatakana: z.string().optional(),
portingGender: z.enum(PORTING_GENDER_VALUES).optional(),
portingDateOfBirth: z.string().optional(),
address: z
.object({
street: z.string().optional(),
streetLine2: z.string().optional(),
city: z.string().optional(),
state: z.string().optional(),
postalCode: z.string().optional(),
country: z.string().optional(),
})
.optional(),
})
.passthrough();
export type OrderSelections = z.infer<typeof orderSelectionsSchema>;
const baseCreateOrderSchema = z.object({
orderType: z.enum(["Internet", "SIM", "VPN", "Other"]),
skus: z.array(z.string()),
configurations: orderConfigurationsSchema.optional(),
});
export const createOrderRequestSchema = baseCreateOrderSchema;
export const orderBusinessValidationSchema =
baseCreateOrderSchema
.extend({
userId: z.string().uuid(),
opportunityId: z.string().optional(),
})
.refine(
(data) => {
if (data.orderType === "Internet") {
const mainServiceSkus = data.skus.filter(sku => {
const upperSku = sku.toUpperCase();
return (
!upperSku.includes("INSTALL") &&
!upperSku.includes("ADDON") &&
!upperSku.includes("ACTIVATION") &&
!upperSku.includes("FEE")
);
});
return mainServiceSkus.length >= 1;
}
return true;
},
{
message: "Internet orders must have at least one main service SKU (non-installation, non-addon)",
path: ["skus"],
}
)
.refine(
(data) => {
if (data.orderType === "SIM" && data.configurations) {
return data.configurations.simType !== undefined;
}
return true;
},
{
message: "SIM orders must specify SIM type",
path: ["configurations", "simType"],
}
)
.refine(
(data) => {
if (data.configurations?.simType === "eSIM") {
return data.configurations.eid !== undefined && data.configurations.eid.length > 0;
}
return true;
},
{
message: "eSIM orders must provide EID",
path: ["configurations", "eid"],
}
)
.refine(
(data) => {
if (data.configurations?.isMnp === "true") {
const required = [
"mnpNumber",
"portingLastName",
"portingFirstName",
] as const;
return required.every(field => data.configurations?.[field] !== undefined);
}
return true;
},
{
message: "MNP orders must provide porting information",
path: ["configurations"],
}
);
export const sfOrderIdParamSchema = z.object({
sfOrderId: z
.string()
.length(18, "Salesforce order ID must be 18 characters")
.regex(/^[A-Za-z0-9]+$/, "Salesforce order ID must be alphanumeric"),
});
export type SfOrderIdParam = z.infer<typeof sfOrderIdParamSchema>;
// ============================================================================
// Checkout Schemas
// ============================================================================
/**
* Schema for individual checkout items
*/
export const checkoutItemSchema = z.object({
id: z.string(),
sku: z.string(),
name: z.string(),
description: z.string().optional(),
monthlyPrice: z.number().optional(),
oneTimePrice: z.number().optional(),
quantity: z.number().positive(),
itemType: z.enum(['plan', 'installation', 'addon', 'activation', 'vpn']),
autoAdded: z.boolean().optional(),
});
/**
* Schema for checkout totals
*/
export const checkoutTotalsSchema = z.object({
monthlyTotal: z.number(),
oneTimeTotal: z.number(),
});
/**
* Schema for complete checkout cart
*/
export const checkoutCartSchema = z.object({
items: z.array(checkoutItemSchema),
totals: checkoutTotalsSchema,
configuration: orderConfigurationsSchema,
});
export const checkoutBuildCartRequestSchema = z.object({
orderType: z.enum(["Internet", "SIM", "VPN", "Other"]),
selections: orderSelectionsSchema,
configuration: orderConfigurationsSchema.optional(),
});
export const checkoutBuildCartResponseSchema = apiSuccessResponseSchema(checkoutCartSchema);
/**
* Schema for order creation response
*/
export const orderCreateResponseSchema = z.object({
sfOrderId: z.string(),
status: z.string(),
message: z.string(),
});
// ============================================================================
// Inferred Types from Schemas (Schema-First Approach)
// ============================================================================
// Order item types
export type OrderItemSummary = z.infer<typeof orderItemSummarySchema>;
export type OrderItemDetails = z.infer<typeof orderItemDetailsSchema>;
// Order types
export type OrderSummary = z.infer<typeof orderSummarySchema>;
export type OrderDetails = z.infer<typeof orderDetailsSchema>;
// Query and creation types
export type OrderQueryParams = z.infer<typeof orderQueryParamsSchema>;
export type OrderConfigurationsAddress = z.infer<typeof orderConfigurationsAddressSchema>;
export type OrderConfigurations = z.infer<typeof orderConfigurationsSchema>;
export type CreateOrderRequest = z.infer<typeof createOrderRequestSchema>;
export type OrderBusinessValidation = z.infer<typeof orderBusinessValidationSchema>;
export type CheckoutBuildCartRequest = z.infer<typeof checkoutBuildCartRequestSchema>;
export type CheckoutBuildCartResponse = z.infer<typeof checkoutBuildCartResponseSchema>;
// ============================================================================
// Order Display Types (for UI presentation)
// ============================================================================
export const orderDisplayItemCategorySchema = z.enum([
"service",
"installation",
"addon",
"activation",
"other",
]);
export const orderDisplayItemChargeKindSchema = z.enum([
"monthly",
"one-time",
"other",
]);
export const orderDisplayItemChargeSchema = z.object({
kind: orderDisplayItemChargeKindSchema,
amount: z.number(),
label: z.string(),
suffix: z.string().optional(),
});
export const orderDisplayItemSchema = z.object({
id: z.string(),
name: z.string(),
quantity: z.number().optional(),
status: z.string().optional(),
primaryCategory: orderDisplayItemCategorySchema,
categories: z.array(orderDisplayItemCategorySchema),
charges: z.array(orderDisplayItemChargeSchema),
included: z.boolean(),
sourceItems: z.array(orderItemSummarySchema),
isBundle: z.boolean(),
});
// Types
export type OrderDisplayItemCategory = z.infer<typeof orderDisplayItemCategorySchema>;
export type OrderDisplayItemChargeKind = z.infer<typeof orderDisplayItemChargeKindSchema>;
export type OrderDisplayItemCharge = z.infer<typeof orderDisplayItemChargeSchema>;
export type OrderDisplayItem = z.infer<typeof orderDisplayItemSchema>;