EID input now strips non-numeric characters, shows a digit counter warning while typing, and enforces exactly 32 digits via Zod schemas across domain, order configurations, and order selections layers. Also installs missing input-otp dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
449 lines
15 KiB
TypeScript
449 lines
15 KiB
TypeScript
/**
|
|
* Orders Domain - Schemas
|
|
*
|
|
* Zod schemas for runtime validation of order data.
|
|
*/
|
|
|
|
import { z } from "zod";
|
|
|
|
// ============================================================================
|
|
// 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(),
|
|
opportunityId: z.string().optional(), // Linked Opportunity for lifecycle tracking
|
|
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()
|
|
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
|
|
.optional(),
|
|
isMnp: z.string().optional(),
|
|
mnpNumber: z.string().optional(),
|
|
mnpExpiry: z.string().optional(),
|
|
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").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()
|
|
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
|
|
.optional(),
|
|
isMnp: z.string().optional(),
|
|
mnpNumber: z.string().optional(),
|
|
mnpExpiry: z.string().optional(),
|
|
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").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(),
|
|
})
|
|
.strip();
|
|
|
|
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 = checkoutCartSchema;
|
|
|
|
// ============================================================================
|
|
// BFF endpoint request/param schemas (DTO inputs)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Body for POST /orders/from-checkout-session
|
|
*/
|
|
export const checkoutSessionCreateOrderRequestSchema = z.object({
|
|
checkoutSessionId: z.string().uuid(),
|
|
});
|
|
|
|
/**
|
|
* Params for GET /checkout/session/:sessionId
|
|
*/
|
|
export const checkoutSessionIdParamSchema = z.object({
|
|
sessionId: z.string().uuid(),
|
|
});
|
|
|
|
// ============================================================================
|
|
// BFF endpoint response schemas (shared contracts)
|
|
// ============================================================================
|
|
|
|
export const checkoutCartSummarySchema = z.object({
|
|
items: z.array(checkoutItemSchema),
|
|
totals: checkoutTotalsSchema,
|
|
});
|
|
|
|
export const checkoutSessionDataSchema = z.object({
|
|
sessionId: z.string().uuid(),
|
|
expiresAt: z.string(),
|
|
orderType: z.enum(["Internet", "SIM", "VPN", "Other"]),
|
|
cart: checkoutCartSummarySchema,
|
|
});
|
|
|
|
export const checkoutSessionResponseSchema = checkoutSessionDataSchema;
|
|
|
|
export const checkoutValidateCartDataSchema = z.object({ valid: z.boolean() });
|
|
export const checkoutValidateCartResponseSchema = checkoutValidateCartDataSchema;
|
|
|
|
/**
|
|
* Schema for order creation response
|
|
*/
|
|
export const orderCreateResponseSchema = z.object({
|
|
sfOrderId: z.string(),
|
|
status: z.string(),
|
|
message: z.string(),
|
|
});
|
|
|
|
export const orderListResponseSchema = z.array(orderSummarySchema);
|
|
|
|
export type OrderListResponse = z.infer<typeof orderListResponseSchema>;
|
|
|
|
// ============================================================================
|
|
// 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 CheckoutItem = z.infer<typeof checkoutItemSchema>;
|
|
export type CheckoutTotals = z.infer<typeof checkoutTotalsSchema>;
|
|
export type CheckoutCart = z.infer<typeof checkoutCartSchema>;
|
|
export type OrderCreateResponse = z.infer<typeof orderCreateResponseSchema>;
|
|
export type CheckoutBuildCartRequest = z.infer<typeof checkoutBuildCartRequestSchema>;
|
|
export type CheckoutBuildCartResponse = z.infer<typeof checkoutBuildCartResponseSchema>;
|
|
export type CheckoutSessionCreateOrderRequest = z.infer<
|
|
typeof checkoutSessionCreateOrderRequestSchema
|
|
>;
|
|
export type CheckoutSessionIdParam = z.infer<typeof checkoutSessionIdParamSchema>;
|
|
export type CheckoutCartSummary = z.infer<typeof checkoutCartSummarySchema>;
|
|
export type CheckoutSessionResponse = z.infer<typeof checkoutSessionResponseSchema>;
|
|
export type CheckoutValidateCartResponse = z.infer<typeof checkoutValidateCartResponseSchema>;
|
|
|
|
// ============================================================================
|
|
// 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>;
|