/** * Orders Domain - Schemas * * Zod schemas for runtime validation of order data. */ import { z } from "zod"; import { apiSuccessResponseSchema } from "../common/index.js"; // ============================================================================ // 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; 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; // ============================================================================ // 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; export type OrderItemDetails = z.infer; // Order types export type OrderSummary = z.infer; export type OrderDetails = z.infer; // Query and creation types export type OrderQueryParams = z.infer; export type OrderConfigurationsAddress = z.infer; export type OrderConfigurations = z.infer; export type CreateOrderRequest = z.infer; export type OrderBusinessValidation = z.infer; export type CheckoutBuildCartRequest = z.infer; export type CheckoutBuildCartResponse = z.infer; // ============================================================================ // 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; export type OrderDisplayItemChargeKind = z.infer; export type OrderDisplayItemCharge = z.infer; export type OrderDisplayItem = z.infer;