Order wizard was skipping steps (jumping to add-ons) due to stale currentStep persisting in localStorage from previous orders. Reset store on plan selection and exclude currentStep from persistence. Also add max(11) validation on MNP phone number to prevent Salesforce STRING_TOO_LONG errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
443 lines
15 KiB
TypeScript
443 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().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().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>;
|