- Replaced multiple global exception filters with a unified exception filter to streamline error handling across the application. - Removed deprecated AuthErrorFilter and GlobalExceptionFilter to reduce redundancy. - Enhanced SupportController to include new endpoints for listing, retrieving, and creating support cases, improving the support case management functionality. - Integrated SalesforceCaseService for better interaction with Salesforce data in support case operations. - Updated support case schemas to align with new requirements and ensure data consistency.
197 lines
6.0 KiB
TypeScript
197 lines
6.0 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,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|