Temuulen Ankhbayar 1ef2c5e125 fix: resolve SF field type mismatches, date format conversions, and gender mapping
- MNP_Application__c: change Zod type from z.string() to z.coerce.boolean()
  to match Salesforce Checkbox field type
- Date fields (mnpExpiry, portingDateOfBirth, scheduledAt): convert
  YYYY-MM-DD back to YYYYMMDD when reading from SF for Freebit API
- Gender mapping: handle full SF picklist values (Male/Female/Corporate)
  not just legacy M/F codes, for both Freebit activation and PA05-05
- MNP reservation number: add max(10) per SF Text(10) field limit
- Porting name fields: add max(255) to match SF Text(255) limits

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:38:57 +09:00

449 lines
16 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().max(10, "MNP reservation number must be 10 digits or fewer").optional(),
mnpExpiry: z.string().optional(),
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
mvnoAccountNumber: z.string().max(255).optional(),
portingLastName: z.string().max(255).optional(),
portingFirstName: z.string().max(255).optional(),
portingLastNameKatakana: z.string().max(255).optional(),
portingFirstNameKatakana: z.string().max(255).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().max(10, "MNP reservation number must be 10 digits or fewer").optional(),
mnpExpiry: z.string().optional(),
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
mvnoAccountNumber: z.string().max(255).optional(),
portingLastName: z.string().max(255).optional(),
portingFirstName: z.string().max(255).optional(),
portingLastNameKatakana: z.string().max(255).optional(),
portingFirstNameKatakana: z.string().max(255).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>;