barsa 67691a50b5 Refactor WHMCS service and improve address handling
- Updated WHMCS service to utilize addressSchema for client address retrieval, enhancing data validation.
- Cleaned up imports in WHMCS client service for better organization.
- Improved logging format in WHMCS subscription service for clearer output.
- Streamlined address handling in order builder service for consistency.
- Enhanced code readability by formatting JSX elements in ProfileContainer and other components.
- Removed deprecated billing cycle normalization logic and centralized it in the domain helpers for better maintainability.
2025-11-04 11:14:26 +09:00

141 lines
4.4 KiB
TypeScript

import {
orderConfigurationsSchema,
orderSelectionsSchema,
type OrderConfigurations,
type OrderSelections,
} from "./schema";
import type { SimConfigureFormData } from "../sim";
import type { WhmcsOrderItem } from "./providers/whmcs/raw.types";
export interface BuildSimOrderConfigurationsOptions {
/**
* Optional fallback phone number when the SIM form does not include it directly.
* Useful for flows where the phone number is collected separately.
*/
phoneNumber?: string | null | undefined;
}
const normalizeString = (value: unknown): string | undefined => {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
};
const normalizeDate = (value: unknown): string | undefined => {
const str = normalizeString(value);
if (!str) return undefined;
return str.replace(/-/g, "");
};
type BillingCycle = WhmcsOrderItem["billingCycle"];
const BILLING_CYCLE_ALIASES: Record<string, BillingCycle> = {
monthly: "monthly",
month: "monthly",
onetime: "onetime",
once: "onetime",
singlepayment: "onetime",
annual: "annually",
annually: "annually",
yearly: "annually",
year: "annually",
quarterly: "quarterly",
quarter: "quarterly",
qtr: "quarterly",
semiannual: "semiannually",
semiannually: "semiannually",
semiannualy: "semiannually",
semiannualpayment: "semiannually",
semiannualbilling: "semiannually",
biannual: "semiannually",
biannually: "semiannually",
biennial: "biennially",
biennially: "biennially",
triennial: "triennially",
triennially: "triennially",
free: "free",
};
const normalizeBillingCycleKey = (value: string): string =>
value.trim().toLowerCase().replace(/[\s_-]+/g, "");
const DEFAULT_BILLING_CYCLE: BillingCycle = "monthly";
export interface NormalizeBillingCycleOptions {
defaultValue?: BillingCycle;
}
/**
* Normalize arbitrary billing cycle strings to the canonical WHMCS values.
* Keeps mapping logic in the domain so both BFF and UI stay in sync.
*/
export function normalizeBillingCycle(
value: unknown,
options: NormalizeBillingCycleOptions = {}
): BillingCycle {
if (typeof value !== "string") {
return options.defaultValue ?? DEFAULT_BILLING_CYCLE;
}
const directKey = normalizeBillingCycleKey(value);
const matched = BILLING_CYCLE_ALIASES[directKey];
if (matched) {
return matched;
}
return options.defaultValue ?? DEFAULT_BILLING_CYCLE;
}
/**
* Build an OrderConfigurations object for SIM orders from the shared SimConfigureFormData.
* Ensures the resulting payload conforms to the domain schema before it is sent to the BFF.
*/
export function buildSimOrderConfigurations(
formData: SimConfigureFormData,
options: BuildSimOrderConfigurationsOptions = {}
): OrderConfigurations {
const base: Record<string, unknown> = {
simType: formData.simType,
activationType: formData.activationType,
};
const eid = normalizeString(formData.eid);
if (formData.simType === "eSIM" && eid) {
base.eid = eid;
}
const scheduledDate = normalizeDate(formData.scheduledActivationDate);
if (formData.activationType === "Scheduled" && scheduledDate) {
base.scheduledAt = scheduledDate;
}
const phoneCandidate =
normalizeString(formData.mnpData?.phoneNumber) ?? normalizeString(options.phoneNumber);
if (phoneCandidate) {
base.mnpPhone = phoneCandidate;
}
if (formData.wantsMnp && formData.mnpData) {
const mnp = formData.mnpData;
base.isMnp = "true";
base.mnpNumber = normalizeString(mnp.reservationNumber);
base.mnpExpiry = normalizeDate(mnp.expiryDate);
base.mvnoAccountNumber = normalizeString(mnp.mvnoAccountNumber);
base.portingLastName = normalizeString(mnp.portingLastName);
base.portingFirstName = normalizeString(mnp.portingFirstName);
base.portingLastNameKatakana = normalizeString(mnp.portingLastNameKatakana);
base.portingFirstNameKatakana = normalizeString(mnp.portingFirstNameKatakana);
base.portingGender = normalizeString(mnp.portingGender);
base.portingDateOfBirth = normalizeDate(mnp.portingDateOfBirth);
} else if (formData.wantsMnp) {
// When wantsMnp is true but data is missing, mark the flag so validation fails clearly downstream.
base.isMnp = "true";
}
return orderConfigurationsSchema.parse(base);
}
export function normalizeOrderSelections(value: unknown): OrderSelections {
return orderSelectionsSchema.parse(value);
}