barsa 38bb40b88b Add Service and Component Structure for Internet and SIM Offerings
- Introduced new controllers for internet eligibility and service health checks to enhance backend functionality.
- Created service modules for internet, SIM, and VPN offerings, improving organization and maintainability.
- Developed various components for internet and SIM configuration, including forms and plan cards, to streamline user interactions.
- Implemented hooks for managing service configurations and eligibility checks, enhancing frontend data handling.
- Updated utility functions for pricing and catalog operations to support new service structures and improve performance.
2025-12-25 13:20:45 +09:00

198 lines
6.1 KiB
TypeScript

import {
internetCatalogResponseSchema,
internetPlanCatalogItemSchema,
simCatalogResponseSchema,
vpnCatalogResponseSchema,
type InternetCatalogCollection,
type InternetPlanCatalogItem,
type SimCatalogCollection,
type VpnCatalogCollection,
type InternetPlanTemplate,
type CatalogProductBase,
} from "./schema.js";
import type { CatalogPriceInfo } from "./contract.js";
/**
* 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,
};
}
/**
* Calculate savings percentage between original and current price
* Returns 0 if there are no savings (current >= original)
*/
export function calculateSavingsPercentage(originalPrice: number, currentPrice: number): number {
if (originalPrice <= currentPrice) return 0;
return Math.round(((originalPrice - currentPrice) / originalPrice) * 100);
}