barsa c497eae763 Enhance catalog caching and pricing utilities
- Introduced a new interface, LegacyCatalogCachePayload, to improve cache handling in CatalogCacheService, allowing for better normalization of cached values.
- Updated the getDisplayPrice function to utilize a centralized price formatting utility, getCatalogProductPriceDisplay, for consistent price rendering across the application.
- Refactored order preparation logic in useCheckout to leverage a new domain helper, prepareOrderFromCart, streamlining SKU extraction and payload formatting.
- Added CatalogPriceInfo interface to standardize pricing display information across the frontend and backend.
2025-11-21 15:59:14 +09:00

188 lines
5.7 KiB
TypeScript

import {
internetCatalogResponseSchema,
internetPlanCatalogItemSchema,
simCatalogResponseSchema,
vpnCatalogResponseSchema,
type InternetCatalogCollection,
type InternetPlanCatalogItem,
type SimCatalogCollection,
type VpnCatalogCollection,
type InternetPlanTemplate,
type CatalogProductBase,
} from "./schema";
import type { CatalogPriceInfo } from "./contract";
/**
* Empty catalog defaults shared by portal and BFF.
*/
export const EMPTY_INTERNET_CATALOG: InternetCatalogCollection = {
plans: [],
installations: [],
addons: [],
};
export const EMPTY_SIM_CATALOG: SimCatalogCollection = {
plans: [],
activationFees: [],
addons: [],
};
export const EMPTY_VPN_CATALOG: VpnCatalogCollection = {
plans: [],
activationFees: [],
};
/**
* Safe parser helpers for catalog payloads coming from HTTP boundaries.
*/
export function parseInternetCatalog(data: unknown): InternetCatalogCollection {
return internetCatalogResponseSchema.parse(data);
}
export function parseSimCatalog(data: unknown): SimCatalogCollection {
return simCatalogResponseSchema.parse(data);
}
export function parseVpnCatalog(data: unknown): VpnCatalogCollection {
return vpnCatalogResponseSchema.parse(data);
}
/**
* Internet tier metadata map shared between BFF and portal presenters.
*/
const INTERNET_TIER_METADATA: Record<string, InternetPlanTemplate> = {
silver: {
tierDescription: "Simple package with broadband-modem and ISP only",
description: "Simple package with broadband-modem and ISP only",
features: [
"NTT modem + ISP connection",
"Two ISP connection protocols: IPoE (recommended) or PPPoE",
"Self-configuration of router (you provide your own)",
"Monthly: ¥6,000 | One-time: ¥22,800",
],
},
gold: {
tierDescription: "Standard all-inclusive package with basic Wi-Fi",
description: "Standard all-inclusive package with basic Wi-Fi",
features: [
"NTT modem + wireless router (rental)",
"ISP (IPoE) configured automatically within 24 hours",
"Basic wireless router included",
"Optional: TP-LINK RE650 range extender (¥500/month)",
"Monthly: ¥6,500 | One-time: ¥22,800",
],
},
platinum: {
tierDescription: "Tailored set up with premier Wi-Fi management support",
description:
"Tailored set up with premier Wi-Fi management support - Recommended for homes & apartments larger than 50m²",
features: [
"NTT modem + Netgear INSIGHT Wi-Fi routers",
"Cloud management support for remote router management",
"Automatic updates and quicker support",
"Seamless wireless network setup",
"Monthly: ¥6,500 | One-time: ¥22,800",
"Cloud management: ¥500/month per router",
],
},
};
const DEFAULT_PLAN_TEMPLATE: InternetPlanTemplate = {
tierDescription: "Standard plan",
description: undefined,
features: undefined,
};
export function getInternetTierTemplate(tier?: string | null): InternetPlanTemplate {
if (!tier) {
return DEFAULT_PLAN_TEMPLATE;
}
const normalized = tier.trim().toLowerCase();
return INTERNET_TIER_METADATA[normalized] ?? {
tierDescription: `${tier} plan`,
description: undefined,
features: undefined,
};
}
export type InternetInstallationTerm = "One-time" | "12-Month" | "24-Month";
export function inferInstallationTermFromSku(sku: string): InternetInstallationTerm {
const normalized = sku.toLowerCase();
if (normalized.includes("24")) return "24-Month";
if (normalized.includes("12")) return "12-Month";
return "One-time";
}
export type InternetAddonType = "hikari-denwa-service" | "hikari-denwa-installation" | "other";
export function inferAddonTypeFromSku(sku: string): InternetAddonType {
const upperSku = sku.toUpperCase();
const isDenwa =
upperSku.includes("DENWA") ||
upperSku.includes("HOME-PHONE") ||
upperSku.includes("PHONE");
if (!isDenwa) {
return "other";
}
const isInstallation =
upperSku.includes("INSTALL") || upperSku.includes("SETUP") || upperSku.includes("ACTIVATION");
return isInstallation ? "hikari-denwa-installation" : "hikari-denwa-service";
}
/**
* Helper to apply tier metadata to a plan item.
*/
export function enrichInternetPlanMetadata(plan: InternetPlanCatalogItem): InternetPlanCatalogItem {
const template = getInternetTierTemplate(plan.internetPlanTier ?? null);
const existingMetadata = plan.catalogMetadata ?? {};
const metadata = {
...existingMetadata,
tierDescription: existingMetadata.tierDescription ?? template.tierDescription,
features: existingMetadata.features ?? template.features,
isRecommended:
existingMetadata.isRecommended ?? (plan.internetPlanTier?.toLowerCase() === "gold" ? true : undefined),
};
return internetPlanCatalogItemSchema.parse({
...plan,
description: plan.description ?? template.description,
catalogMetadata: metadata,
features: plan.features ?? template.features,
});
}
export const internetPlanCollectionSchema = internetPlanCatalogItemSchema.array();
/**
* Calculates display price information for a catalog item
* Centralized logic for price formatting
*/
export function getCatalogProductPriceDisplay(item: CatalogProductBase): CatalogPriceInfo | null {
const monthlyPrice = item.monthlyPrice ?? null;
const oneTimePrice = item.oneTimePrice ?? null;
const currency = "JPY";
if (monthlyPrice === null && oneTimePrice === null) {
return null;
}
let display = "";
if (monthlyPrice !== null && monthlyPrice > 0) {
display = `¥${monthlyPrice.toLocaleString()}/month`;
} else if (oneTimePrice !== null && oneTimePrice > 0) {
display = `¥${oneTimePrice.toLocaleString()} (one-time)`;
}
return {
display,
monthly: monthlyPrice,
oneTime: oneTimePrice,
currency,
};
}