barsa d9734b0c82 Enhance Signup Workflow and Update Catalog Components
- Refactored the SignupWorkflowService to throw a DomainHttpException for legacy account conflicts, improving error handling.
- Updated the SignupForm component to include initialEmail and showFooterLinks props, enhancing user experience during account creation.
- Improved the AccountStep in the SignupForm to allow users to add optional details, such as date of birth and gender, for a more personalized signup process.
- Enhanced the PasswordStep to include terms acceptance and marketing consent options, ensuring compliance and user engagement.
- Updated various catalog views to improve layout and user guidance, streamlining the onboarding process for new users.
2025-12-22 18:59:38 +09:00

390 lines
13 KiB
TypeScript

/**
* 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(),
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().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>;