Refactor catalog services and update product types for improved clarity and functionality

- Updated Internet and SIM catalog services to utilize new product types, enhancing type safety and consistency.
- Refactored methods to return specific catalog item types, including InternetPlanCatalogItem, InternetInstallationCatalogItem, and SimActivationFeeCatalogItem.
- Adjusted API responses and frontend components to align with updated product structures, ensuring accurate data handling.
- Removed deprecated fields and streamlined type definitions across the catalog services and related components.
This commit is contained in:
T. Narantuya 2025-09-18 11:22:22 +09:00
parent a95ec60859
commit 52adc29016
13 changed files with 353 additions and 312 deletions

View File

@ -1,13 +1,17 @@
import { Controller, Get, Request } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiBearerAuth } from "@nestjs/swagger";
import { InternetCatalogService } from "./services/internet-catalog.service";
import { SimCatalogService } from "./services/sim-catalog.service";
import { VpnCatalogService } from "./services/vpn-catalog.service";
import {
InternetProduct,
SimProduct,
VpnProduct,
} from "@customer-portal/domain";
InternetAddonCatalogItem,
InternetCatalogService,
InternetInstallationCatalogItem,
InternetPlanCatalogItem,
} from "./services/internet-catalog.service";
import {
SimActivationFeeCatalogItem,
SimCatalogService,
} from "./services/sim-catalog.service";
import { VpnCatalogService } from "./services/vpn-catalog.service";
import { SimProduct, VpnProduct } from "@customer-portal/domain";
@ApiTags("catalog")
@Controller("catalog")
@ -21,7 +25,9 @@ export class CatalogController {
@Get("internet/plans")
@ApiOperation({ summary: "Get Internet plans filtered by customer eligibility" })
async getInternetPlans(@Request() req: { user: { id: string } }): Promise<InternetProduct[]> {
async getInternetPlans(
@Request() req: { user: { id: string } }
): Promise<InternetPlanCatalogItem[]> {
const userId = req.user?.id;
if (!userId) {
// Fallback to all plans if no user context
@ -32,13 +38,13 @@ export class CatalogController {
@Get("internet/addons")
@ApiOperation({ summary: "Get Internet add-ons" })
async getInternetAddons(): Promise<InternetProduct[]> {
async getInternetAddons(): Promise<InternetAddonCatalogItem[]> {
return this.internetCatalog.getAddons();
}
@Get("internet/installations")
@ApiOperation({ summary: "Get Internet installations" })
async getInternetInstallations(): Promise<InternetProduct[]> {
async getInternetInstallations(): Promise<InternetInstallationCatalogItem[]> {
return this.internetCatalog.getInstallations();
}
@ -49,14 +55,14 @@ export class CatalogController {
if (!userId) {
// Fallback to all regular plans if no user context
const allPlans = await this.simCatalog.getPlans();
return allPlans.filter(plan => !plan.hasFamilyDiscount);
return allPlans.filter(plan => !plan.simHasFamilyDiscount);
}
return this.simCatalog.getPlansForUser(userId);
}
@Get("sim/activation-fees")
@ApiOperation({ summary: "Get SIM activation fees" })
async getSimActivationFees(): Promise<SimProduct[]> {
async getSimActivationFees(): Promise<SimActivationFeeCatalogItem[]> {
return this.simCatalog.getActivationFees();
}

View File

@ -1,7 +1,6 @@
import { Injectable, Inject } from "@nestjs/common";
import { BaseCatalogService } from "./base-catalog.service";
import { InternetProduct, fromSalesforceProduct2, SalesforceProduct2Record } from "@customer-portal/domain";
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service";
import { MappingsService } from "../../id-mappings/mappings.service";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
import { Logger } from "nestjs-pino";
@ -12,18 +11,39 @@ interface SalesforceAccount {
Internet_Eligibility__c?: string;
}
export type InternetPlanCatalogItem = InternetProduct & {
catalogMetadata: {
tierDescription: string;
features: string[];
isRecommended: boolean;
};
};
export type InternetInstallationCatalogItem = InternetProduct & {
catalogMetadata: {
installationTerm: "One-time" | "12-Month" | "24-Month";
};
};
export type InternetAddonCatalogItem = InternetProduct & {
catalogMetadata: {
addonCategory: "hikari-denwa-service" | "hikari-denwa-installation" | "other";
autoAdd: boolean;
requiredWith: string[];
};
};
@Injectable()
export class InternetCatalogService extends BaseCatalogService {
constructor(
sf: SalesforceConnection,
@Inject(Logger) logger: Logger,
private salesforceService: SalesforceService,
private mappingsService: MappingsService
) {
super(sf, logger);
}
async getPlans(): Promise<InternetProduct[]> {
async getPlans(): Promise<InternetPlanCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildCatalogServiceQuery("Internet", [
fields.product.internetPlanTier,
@ -44,15 +64,17 @@ export class InternetCatalogService extends BaseCatalogService {
return {
...product,
tierDescription: tierData.tierDescription,
features: tierData.features,
isRecommended: product.internetPlanTier === "Gold",
catalogMetadata: {
tierDescription: tierData.tierDescription,
features: tierData.features,
isRecommended: product.internetPlanTier === "Gold",
},
description: product.description ?? tierData.description,
} satisfies InternetProduct;
} satisfies InternetPlanCatalogItem;
});
}
async getInstallations(): Promise<InternetProduct[]> {
async getInstallations(): Promise<InternetInstallationCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("Internet", "Installation", [
fields.product.billingCycle,
@ -72,22 +94,19 @@ export class InternetCatalogService extends BaseCatalogService {
) as InternetProduct;
const installationType = this.inferInstallationTypeFromSku(product.sku);
const priceValue =
product.billingCycle === "Monthly"
? product.monthlyPrice ?? product.unitPrice ?? 0
: product.oneTimePrice ?? product.unitPrice ?? 0;
return {
...product,
type: installationType,
price: priceValue,
catalogMetadata: {
installationTerm: installationType,
},
displayOrder: product.displayOrder ?? Number(record[fields.product.displayOrder] ?? 0),
} satisfies InternetProduct;
} satisfies InternetInstallationCatalogItem;
})
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
}
async getAddons(): Promise<InternetProduct[]> {
async getAddons(): Promise<InternetAddonCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("Internet", "Add-on", [
fields.product.billingCycle,
@ -109,19 +128,16 @@ export class InternetCatalogService extends BaseCatalogService {
) as InternetProduct;
const addonType = this.inferAddonTypeFromSku(product.sku);
const activationPrice =
product.billingCycle === "Onetime"
? product.oneTimePrice ?? product.unitPrice ?? 0
: undefined;
return {
...product,
type: addonType,
activationPrice,
autoAdd: false,
requiredWith: [],
catalogMetadata: {
addonCategory: addonType,
autoAdd: false,
requiredWith: [],
},
displayOrder: product.displayOrder ?? Number(record[fields.product.displayOrder] ?? 0),
} satisfies InternetProduct;
} satisfies InternetAddonCatalogItem;
})
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
}
@ -135,7 +151,7 @@ export class InternetCatalogService extends BaseCatalogService {
return { plans, installations, addons };
}
async getPlansForUser(userId: string): Promise<InternetProduct[]> {
async getPlansForUser(userId: string): Promise<InternetPlanCatalogItem[]> {
try {
// Get all plans first
const allPlans = await this.getPlans();
@ -162,7 +178,7 @@ export class InternetCatalogService extends BaseCatalogService {
if (!eligibility) {
this.logger.log(`No eligibility field for user ${userId}, filtering to Home 1G plans only`);
const homeGPlans = allPlans.filter(plan => plan.offeringType === "Home 1G");
const homeGPlans = allPlans.filter(plan => plan.internetOfferingType === "Home 1G");
return homeGPlans;
}
@ -171,7 +187,7 @@ export class InternetCatalogService extends BaseCatalogService {
const isEligible = this.checkPlanEligibility(plan, eligibility);
if (!isEligible) {
this.logger.debug(
`Plan ${plan.name} (${plan.tier}) not eligible for user ${userId} with eligibility: ${eligibility}`
`Plan ${plan.name} (${plan.internetPlanTier ?? "Unknown"}) not eligible for user ${userId} with eligibility: ${eligibility}`
);
}
return isEligible;
@ -190,10 +206,10 @@ export class InternetCatalogService extends BaseCatalogService {
}
}
private checkPlanEligibility(plan: InternetProduct, eligibility: string): boolean {
// Simple match: user's eligibility field must equal plan's offering type
// e.g., eligibility "Home 1G" matches plan.offeringType "Home 1G"
return plan.offeringType === eligibility;
private checkPlanEligibility(plan: InternetPlanCatalogItem, eligibility: string): boolean {
// Simple match: user's eligibility field must equal plan's Salesforce offering type
// e.g., eligibility "Home 1G" matches plan.internetOfferingType "Home 1G"
return plan.internetOfferingType === eligibility;
}
private getTierData(tier: string) {
@ -239,4 +255,35 @@ export class InternetCatalogService extends BaseCatalogService {
return tierData[tier] || tierData["Silver"];
}
private inferInstallationTypeFromSku(sku: string): "One-time" | "12-Month" | "24-Month" {
const upperSku = sku.toUpperCase();
if (upperSku.includes("12M") || upperSku.includes("12-MONTH")) {
return "12-Month";
}
if (upperSku.includes("24M") || upperSku.includes("24-MONTH")) {
return "24-Month";
}
return "One-time";
}
private inferAddonTypeFromSku(
sku: string
): "hikari-denwa-service" | "hikari-denwa-installation" | "other" {
const upperSku = sku.toUpperCase();
if (
upperSku.includes("DENWA") ||
upperSku.includes("HOME-PHONE") ||
upperSku.includes("PHONE")
) {
if (
upperSku.includes("INSTALL") ||
upperSku.includes("SETUP") ||
upperSku.includes("ACTIVATION")
) {
return "hikari-denwa-installation";
}
return "hikari-denwa-service";
}
return "other";
}
}

View File

@ -1,18 +1,22 @@
import { Injectable, Inject } from "@nestjs/common";
import { BaseCatalogService } from "./base-catalog.service";
import { SimProduct, fromSalesforceProduct2, SalesforceProduct2Record } from "@customer-portal/domain";
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service";
import { MappingsService } from "../../id-mappings/mappings.service";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
import { Logger } from "nestjs-pino";
import { WhmcsConnectionService } from "@bff/integrations/whmcs/services/whmcs-connection.service";
export type SimActivationFeeCatalogItem = SimProduct & {
catalogMetadata: {
isDefault: boolean;
};
};
@Injectable()
export class SimCatalogService extends BaseCatalogService {
constructor(
sf: SalesforceConnection,
@Inject(Logger) logger: Logger,
private salesforceService: SalesforceService,
private mappingsService: MappingsService,
private whmcs: WhmcsConnectionService
) {
@ -39,16 +43,13 @@ export class SimCatalogService extends BaseCatalogService {
return {
...product,
dataSize: product.simDataSize || "Unknown",
planType: product.simPlanType,
hasFamilyDiscount: product.simHasFamilyDiscount || false,
description: product.name,
features: product.features ?? [],
} satisfies SimProduct;
});
}
async getActivationFees(): Promise<SimProduct[]> {
async getActivationFees(): Promise<SimActivationFeeCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("SIM", "Activation", []);
const records = await this.executeQuery(soql, "SIM Activation Fees");
@ -61,14 +62,13 @@ export class SimCatalogService extends BaseCatalogService {
fields.product
) as SimProduct;
const priceValue = product.oneTimePrice ?? product.unitPrice ?? 0;
return {
...product,
price: priceValue,
description: product.name,
isDefault: true,
} satisfies SimProduct;
catalogMetadata: {
isDefault: true,
},
} satisfies SimActivationFeeCatalogItem;
});
}
@ -93,10 +93,6 @@ export class SimCatalogService extends BaseCatalogService {
return {
...product,
price:
product.billingCycle === "Monthly"
? product.monthlyPrice ?? product.unitPrice ?? 0
: product.oneTimePrice ?? product.unitPrice ?? 0,
description: product.name,
displayOrder: product.displayOrder ?? Number(record[fields.product.displayOrder] ?? 0),
} satisfies SimProduct;
@ -115,17 +111,17 @@ export class SimCatalogService extends BaseCatalogService {
if (hasExistingSim) {
this.logger.log(`User ${userId} has existing SIM, showing family discount plans`);
// Show family discount plans + regular plans for comparison
return allPlans; // Family plans are included with hasFamilyDiscount: true
return allPlans; // Family plans are included with simHasFamilyDiscount: true
} else {
this.logger.log(`User ${userId} has no existing SIM, showing regular plans only`);
// Show only regular plans (hide family discount plans)
return allPlans.filter(plan => !plan.hasFamilyDiscount);
return allPlans.filter(plan => !plan.simHasFamilyDiscount);
}
} catch (error) {
this.logger.error(`Failed to get personalized SIM plans for user ${userId}`, error);
// Fallback to all regular plans
const allPlans = await this.getPlans();
return allPlans.filter(plan => !plan.hasFamilyDiscount);
return allPlans.filter(plan => !plan.simHasFamilyDiscount);
}
}

View File

@ -85,10 +85,10 @@ function InternetConfigureContent() {
const installationSkuParam = searchParams.get("installationSku");
if (installationSkuParam) {
const installation = installationsData.find(i => i.sku === installationSkuParam);
if (installation) {
setInstallPlan(installation.type);
}
const installation = installationsData.find(i => i.sku === installationSkuParam);
if (installation) {
setInstallPlan(installation.catalogMetadata.installationTerm);
}
}
// Restore selected addons from URL parameters
@ -129,23 +129,34 @@ function InternetConfigureContent() {
// Add selected addons
selectedAddonSkus.forEach(addonSku => {
const addon = addons.find(a => a.sku === addonSku);
if (addon) {
if (addon.monthlyPrice) {
monthlyTotal += addon.monthlyPrice;
}
if (addon.activationPrice) {
oneTimeTotal += addon.activationPrice;
}
if (!addon) return;
const monthlyCharge = addon.monthlyPrice ?? (addon.billingCycle === "Monthly" ? addon.unitPrice : undefined);
const oneTimeCharge = addon.oneTimePrice ?? (addon.billingCycle !== "Monthly" ? addon.unitPrice : undefined);
if (typeof monthlyCharge === "number") {
monthlyTotal += monthlyCharge;
}
if (typeof oneTimeCharge === "number") {
oneTimeTotal += oneTimeCharge;
}
});
// Add installation cost
const installation = installations.find(i => i.type === installPlan);
if (installation && installation.price) {
if (installation.billingCycle === "Monthly") {
monthlyTotal += installation.price;
} else {
oneTimeTotal += installation.price;
const installation = installations.find(
i => i.catalogMetadata.installationTerm === installPlan
);
if (installation) {
const monthlyCharge =
installation.monthlyPrice ?? (installation.billingCycle === "Monthly" ? installation.unitPrice : undefined);
const oneTimeCharge =
installation.oneTimePrice ?? (installation.billingCycle !== "Monthly" ? installation.unitPrice : undefined);
if (typeof monthlyCharge === "number") {
monthlyTotal += monthlyCharge;
}
if (typeof oneTimeCharge === "number") {
oneTimeTotal += oneTimeCharge;
}
}
@ -164,7 +175,9 @@ function InternetConfigureContent() {
});
// Add installation SKU (not type)
const installation = installations.find(i => i.type === installPlan);
const installation = installations.find(
i => i.catalogMetadata.installationTerm === installPlan
);
if (installation) {
params.append("installationSku", installation.sku);
}
@ -229,14 +242,14 @@ function InternetConfigureContent() {
<div className="inline-flex items-center gap-3 bg-gray-50 px-6 py-3 rounded-2xl border">
<div
className={`px-3 py-1 rounded-full text-sm font-medium ${
plan.tier === "Platinum"
plan.internetPlanTier === "Platinum"
? "bg-purple-100 text-purple-800"
: plan.tier === "Gold"
: plan.internetPlanTier === "Gold"
? "bg-yellow-100 text-yellow-800"
: "bg-gray-100 text-gray-800"
}`}
>
{plan.tier}
{plan.internetPlanTier || "Plan"}
</div>
<span className="text-gray-600"></span>
<span className="font-medium text-gray-900">{plan.name}</span>
@ -274,7 +287,7 @@ function InternetConfigureContent() {
</div>
{/* Important Message for Platinum */}
{plan?.tier === "Platinum" && (
{plan?.internetPlanTier === "Platinum" && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<div className="flex items-start">
<div className="flex-shrink-0">
@ -308,7 +321,7 @@ function InternetConfigureContent() {
)}
{/* Access Mode Selection - Only for Silver */}
{plan?.tier === "Silver" ? (
{plan?.internetPlanTier === "Silver" ? (
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-4">
Select Your Router & ISP Configuration:
@ -438,7 +451,7 @@ function InternetConfigureContent() {
/>
</svg>
<span className="font-medium text-green-900">
Access Mode: IPoE-HGW (Pre-configured for {plan?.tier} plan)
Access Mode: IPoE-HGW (Pre-configured for {plan?.internetPlanTier} plan)
</span>
</div>
</div>
@ -477,9 +490,13 @@ function InternetConfigureContent() {
<InstallationOptions
installations={installations}
selectedInstallation={installations.find(inst => inst.type === installPlan) || null}
selectedInstallation={
installations.find(
inst => inst.catalogMetadata.installationTerm === installPlan
) || null
}
onInstallationSelect={installation => {
setInstallPlan(installation.type);
setInstallPlan(installation.catalogMetadata.installationTerm);
}}
showSkus={false}
/>
@ -634,19 +651,19 @@ function InternetConfigureContent() {
<div className="space-y-2">
{selectedAddonSkus.map(addonSku => {
const addon = addons.find(a => a.sku === addonSku);
const monthlyAmount =
addon?.monthlyPrice ?? (addon?.billingCycle === "Monthly" ? addon?.unitPrice : undefined);
const oneTimeAmount =
addon?.oneTimePrice ?? (addon?.billingCycle !== "Monthly" ? addon?.unitPrice : undefined);
const amount = monthlyAmount ?? oneTimeAmount ?? 0;
const cadence = monthlyAmount ? "mo" : "once";
return (
<div key={addonSku} className="flex justify-between text-sm">
<span className="text-gray-600">{addon?.name || addonSku}</span>
<span className="text-gray-900">
¥
{(
addon?.monthlyPrice ||
addon?.activationPrice ||
0
).toLocaleString()}
<span className="text-xs text-gray-500 ml-1">
/{addon?.monthlyPrice ? "mo" : "once"}
</span>
¥{amount.toLocaleString()}
<span className="text-xs text-gray-500 ml-1">/{cadence}</span>
</span>
</div>
);
@ -657,16 +674,26 @@ function InternetConfigureContent() {
{/* Installation Fees */}
{(() => {
const installation = installations.find(i => i.type === installPlan);
return installation && installation.price && installation.price > 0 ? (
const installation = installations.find(
i => i.catalogMetadata.installationTerm === installPlan
);
if (!installation) return null;
const monthlyAmount =
installation.monthlyPrice ?? (installation.billingCycle === "Monthly" ? installation.unitPrice : undefined);
const oneTimeAmount =
installation.oneTimePrice ?? (installation.billingCycle !== "Monthly" ? installation.unitPrice : undefined);
const amount = monthlyAmount ?? oneTimeAmount;
return amount && amount > 0 ? (
<div className="border-t border-gray-200 pt-4 mb-6">
<h4 className="font-medium text-gray-900 mb-3">Installation</h4>
<div className="flex justify-between text-sm">
<span className="text-gray-600">{installation.name}</span>
<span className="text-gray-900">
¥{installation.price.toLocaleString()}
¥{amount.toLocaleString()}
<span className="text-xs text-gray-500 ml-1">
/{installation.billingCycle === "Monthly" ? "mo" : "once"}
/{monthlyAmount ? "mo" : "once"}
</span>
</span>
</div>

View File

@ -42,7 +42,7 @@ export default function InternetPlansPage() {
setPlans(plans);
setInstallations(installations);
if (plans.length > 0) {
setEligibility(plans[0].offeringType || "Home 1G");
setEligibility(plans[0].internetOfferingType || "Home 1G");
}
}
} catch (e) {
@ -58,21 +58,23 @@ export default function InternetPlansPage() {
};
}, []);
const getEligibilityIcon = (offeringType: string) => {
if (offeringType.toLowerCase().includes("home")) {
const getEligibilityIcon = (offeringType?: string) => {
const lower = (offeringType || "").toLowerCase();
if (lower.includes("home")) {
return <HomeIcon className="h-5 w-5" />;
}
if (offeringType.toLowerCase().includes("apartment")) {
if (lower.includes("apartment")) {
return <BuildingOfficeIcon className="h-5 w-5" />;
}
return <HomeIcon className="h-5 w-5" />;
};
const getEligibilityColor = (offeringType: string) => {
if (offeringType.toLowerCase().includes("home")) {
const getEligibilityColor = (offeringType?: string) => {
const lower = (offeringType || "").toLowerCase();
if (lower.includes("home")) {
return "text-blue-600 bg-blue-50 border-blue-200";
}
if (offeringType.toLowerCase().includes("apartment")) {
if (lower.includes("apartment")) {
return "text-green-600 bg-green-50 border-green-200";
}
return "text-gray-600 bg-gray-50 border-gray-200";
@ -200,9 +202,10 @@ function InternetPlanCard({
plan: InternetPlan;
installations: InternetInstallation[];
}) {
const isGold = plan.tier === "Gold";
const isPlatinum = plan.tier === "Platinum";
const isSilver = plan.tier === "Silver";
const tier = plan.internetPlanTier;
const isGold = tier === "Gold";
const isPlatinum = tier === "Platinum";
const isSilver = tier === "Silver";
// Use default variant for all cards to avoid green background on gold
const cardVariant = "default";
@ -215,6 +218,16 @@ function InternetPlanCard({
return "border border-gray-200 shadow-lg hover:shadow-xl";
};
const installationPrices = installations
.map(inst =>
inst.billingCycle === "Monthly"
? inst.monthlyPrice ?? inst.unitPrice
: inst.oneTimePrice ?? inst.unitPrice
)
.filter((price): price is number => typeof price === "number" && Number.isFinite(price));
const minInstallationPrice = installationPrices.length > 0 ? Math.min(...installationPrices) : null;
return (
<AnimatedCard
variant={cardVariant}
@ -233,7 +246,7 @@ function InternetPlanCard({
: "bg-gray-100 text-gray-800 border-gray-300"
}`}
>
{plan.tier}
{tier || "Plan"}
</span>
{isGold && (
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full">
@ -256,14 +269,16 @@ function InternetPlanCard({
{/* Plan Details */}
<h3 className="text-xl font-semibold text-gray-900 mb-2">{plan.name}</h3>
<p className="text-gray-600 text-sm mb-4">{plan.tierDescription || plan.description}</p>
<p className="text-gray-600 text-sm mb-4">
{plan.catalogMetadata.tierDescription || plan.description}
</p>
{/* Your Plan Includes */}
<div className="mb-6 flex-grow">
<h4 className="font-medium text-gray-900 mb-3">Your Plan Includes:</h4>
<ul className="space-y-2 text-sm text-gray-700">
{plan.features && plan.features.length > 0 ? (
plan.features.map((feature, index) => (
{plan.catalogMetadata.features && plan.catalogMetadata.features.length > 0 ? (
plan.catalogMetadata.features.map((feature, index) => (
<li key={index} className="flex items-start">
<span className="text-green-600 mr-2"></span>
{feature}
@ -273,26 +288,26 @@ function InternetPlanCard({
<>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>1 NTT Optical Fiber (Flet&apos;s
Hikari Next - {plan.offeringType?.includes("Apartment") ? "Mansion" : "Home"}{" "}
{plan.offeringType?.includes("10G")
Hikari Next - {plan.internetOfferingType?.includes("Apartment") ? "Mansion" : "Home"}{" "}
{plan.internetOfferingType?.includes("10G")
? "10Gbps"
: plan.offeringType?.includes("100M")
: plan.internetOfferingType?.includes("100M")
? "100Mbps"
: "1Gbps"}
) Installation + Monthly
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
Monthly: ¥{plan.monthlyPrice?.toLocaleString()}
{installations.length > 0 && (
<span className="text-gray-600 text-sm ml-2">
(+ installation from ¥
{Math.min(...installations.map(i => i.price || 0)).toLocaleString()})
</span>
)}
</li>
</>
)}
) Installation + Monthly
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
Monthly: ¥{plan.monthlyPrice?.toLocaleString()}
{minInstallationPrice !== null && (
<span className="text-gray-600 text-sm ml-2">
(+ installation from ¥
{minInstallationPrice.toLocaleString()})
</span>
)}
</li>
</>
)}
</ul>
</div>

View File

@ -381,7 +381,7 @@ function SimConfigureContent() {
<div className="flex items-center gap-2 mb-2">
<DevicePhoneMobileIcon className="h-5 w-5 text-blue-600" />
<h3 className="font-bold text-lg text-gray-900">{plan.name}</h3>
{plan.hasFamilyDiscount && (
{plan.simHasFamilyDiscount && (
<span className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium flex items-center gap-1">
<UsersIcon className="h-3 w-3" />
Family Discount
@ -390,13 +390,13 @@ function SimConfigureContent() {
</div>
<div className="flex items-center gap-4 text-sm text-gray-600 mb-2">
<span>
<strong>Data:</strong> {plan.dataSize}
<strong>Data:</strong> {plan.simDataSize}
</span>
<span>
<strong>Type:</strong>{" "}
{plan.planType === "DataSmsVoice"
{(plan.simPlanType || "DataSmsVoice") === "DataSmsVoice"
? "Data + Voice"
: plan.planType === "DataOnly"
: (plan.simPlanType || "DataSmsVoice") === "DataOnly"
? "Data Only"
: "Voice Only"}
</span>
@ -406,7 +406,7 @@ function SimConfigureContent() {
<div className="text-2xl font-bold text-blue-600">
¥{plan.monthlyPrice?.toLocaleString()}/mo
</div>
{plan.hasFamilyDiscount && (
{plan.simHasFamilyDiscount && (
<div className="text-sm text-green-600 font-medium">Discounted Price</div>
)}
</div>
@ -550,16 +550,16 @@ function SimConfigureContent() {
<div className="mb-6">
<StepHeader
stepNumber={3}
title={plan.planType === "DataOnly" ? "Add-ons" : "Voice Add-ons"}
title={(plan.simPlanType || "DataSmsVoice") === "DataOnly" ? "Add-ons" : "Voice Add-ons"}
description={
plan.planType === "DataOnly"
(plan.simPlanType || "DataSmsVoice") === "DataOnly"
? "No add-ons available for data-only plans"
: "Enhance your voice services with these optional features"
}
/>
</div>
{addons.length > 0 && plan.planType !== "DataOnly" ? (
{addons.length > 0 && (plan.simPlanType || "DataSmsVoice") !== "DataOnly" ? (
<AddonGroup
addons={addons.map(addon => ({
...addon,
@ -579,7 +579,7 @@ function SimConfigureContent() {
) : (
<div className="text-center py-8">
<p className="text-gray-600">
{plan.planType === "DataOnly"
{(plan.simPlanType || "DataSmsVoice") === "DataOnly"
? "No add-ons are available for data-only plans."
: "No add-ons are available for this plan."}
</p>
@ -685,7 +685,7 @@ function SimConfigureContent() {
<div className="flex justify-between items-start">
<div>
<h4 className="font-semibold text-gray-900">{plan.name}</h4>
<p className="text-sm text-gray-600">{plan.dataSize}</p>
<p className="text-sm text-gray-600">{plan.simDataSize}</p>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">
@ -788,7 +788,7 @@ function SimConfigureContent() {
{/* Receipt Footer */}
<div className="text-center mt-6 pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500">
{plan.planType === "DataOnly" && "Data-only service (no voice features)"}
{(plan.simPlanType || "DataSmsVoice") === "DataOnly" && "Data-only service (no voice features)"}
</p>
</div>
</div>

View File

@ -41,8 +41,8 @@ function PlanTypeSection({
if (plans.length === 0) return null;
// Separate regular and family plans
const regularPlans = plans.filter(p => !p.hasFamilyDiscount);
const familyPlans = plans.filter(p => p.hasFamilyDiscount);
const regularPlans = plans.filter(p => !p.simHasFamilyDiscount);
const familyPlans = plans.filter(p => p.simHasFamilyDiscount);
return (
<div className="animate-in fade-in duration-500">
@ -89,7 +89,7 @@ function PlanCard({ plan, isFamily }: { plan: SimPlan; isFamily: boolean }) {
<div>
<div className="flex items-center gap-2 mb-1">
<DevicePhoneMobileIcon className="h-4 w-4 text-blue-600" />
<span className="font-bold text-sm text-gray-900">{plan.dataSize}</span>
<span className="font-bold text-sm text-gray-900">{plan.simDataSize}</span>
</div>
{isFamily && (
<div className="flex items-center gap-1 mb-1">
@ -147,7 +147,7 @@ export default function SimPlansPage() {
if (mounted) {
setPlans(plansData);
// Check if any plans have family discount (indicates user has existing SIM)
setHasExistingSim(plansData.some(p => p.hasFamilyDiscount));
setHasExistingSim(plansData.some(p => p.simHasFamilyDiscount));
}
} catch (e) {
if (mounted) {
@ -196,7 +196,8 @@ export default function SimPlansPage() {
// Group plans by type
const plansByType: PlansByType = plans.reduce(
(acc, plan) => {
acc[plan.planType].push(plan);
const type = plan.simPlanType || "DataSmsVoice";
acc[type].push(plan);
return acc;
},
{

View File

@ -8,7 +8,7 @@ interface AddonItem {
sku: string;
description: string;
monthlyPrice?: number;
activationPrice?: number;
oneTimePrice?: number;
isBundledAddon?: boolean;
bundledAddonId?: string;
displayOrder?: number;
@ -19,7 +19,7 @@ interface AddonGroup {
name: string;
description: string;
monthlyPrice?: number;
activationPrice?: number;
oneTimePrice?: number;
skus: string[];
isBundled: boolean;
}
@ -55,7 +55,7 @@ export function AddonGroup({
if (bundledPartner && !processedAddonIds.has(bundledPartner.id)) {
// Create a combined group
const monthlyAddon = addon.monthlyPrice ? addon : bundledPartner;
const activationAddon = addon.activationPrice ? addon : bundledPartner;
const activationAddon = addon.oneTimePrice ? addon : bundledPartner;
// Generate clean name and description
const cleanName = monthlyAddon.name
@ -68,7 +68,7 @@ export function AddonGroup({
name: bundleName,
description: `${bundleName} (installation included)`,
monthlyPrice: monthlyAddon.monthlyPrice,
activationPrice: activationAddon.activationPrice,
oneTimePrice: activationAddon.oneTimePrice,
skus: [addon.sku, bundledPartner.sku],
isBundled: true,
});
@ -82,7 +82,7 @@ export function AddonGroup({
name: addon.name,
description: addon.description,
monthlyPrice: addon.monthlyPrice,
activationPrice: addon.activationPrice,
oneTimePrice: addon.oneTimePrice,
skus: [addon.sku],
isBundled: false,
});
@ -95,7 +95,7 @@ export function AddonGroup({
name: addon.name,
description: addon.description,
monthlyPrice: addon.monthlyPrice,
activationPrice: addon.activationPrice,
oneTimePrice: addon.oneTimePrice,
skus: [addon.sku],
isBundled: false,
});
@ -155,16 +155,16 @@ export function AddonGroup({
</div>
<p className="text-sm text-gray-600 mt-1">{addonGroup.description}</p>
<div className="flex flex-wrap gap-4 mt-2">
{addonGroup.monthlyPrice && (
<span className="text-sm font-semibold text-blue-600">
¥{addonGroup.monthlyPrice.toLocaleString()}/month
</span>
)}
{addonGroup.activationPrice && (
<span className="text-sm font-semibold text-orange-600">
Activation: ¥{addonGroup.activationPrice.toLocaleString()}
</span>
)}
{addonGroup.monthlyPrice && (
<span className="text-sm font-semibold text-blue-600">
¥{addonGroup.monthlyPrice.toLocaleString()}/month
</span>
)}
{addonGroup.oneTimePrice && (
<span className="text-sm font-semibold text-orange-600">
Activation: ¥{addonGroup.oneTimePrice.toLocaleString()}
</span>
)}
</div>
{addonGroup.isBundled && (
<div className="text-xs text-green-600 mt-1 flex items-center gap-1">

View File

@ -3,7 +3,8 @@ import Link from "next/link";
interface OrderItem {
name: string;
price?: number;
monthlyPrice?: number;
oneTimePrice?: number;
billingCycle?: string;
sku?: string;
}
@ -12,7 +13,7 @@ interface OrderSummaryProps {
// Plan details
plan: {
name: string;
tier?: string;
internetPlanTier?: string;
monthlyPrice?: number;
};
@ -78,7 +79,7 @@ export function OrderSummary({
<span className="text-gray-600">Plan:</span>
<span className="font-medium">
{plan.name}
{plan.tier && ` (${plan.tier})`}
{plan.internetPlanTier && ` (${plan.internetPlanTier})`}
</span>
</div>
@ -107,7 +108,9 @@ export function OrderSummary({
<div className="text-sm font-medium text-gray-700 mb-1">Monthly Costs:</div>
{plan.monthlyPrice && (
<div className="flex justify-between text-sm">
<span className="text-gray-600">Base Plan {plan.tier && `(${plan.tier})`}:</span>
<span className="text-gray-600">
Base Plan {plan.internetPlanTier && `(${plan.internetPlanTier})`}:
</span>
<span className="font-medium">¥{plan.monthlyPrice.toLocaleString()}</span>
</div>
)}
@ -115,10 +118,12 @@ export function OrderSummary({
{selectedAddons.map(
(addon, index) =>
addon.billingCycle === "Monthly" &&
addon.price && (
typeof addon.monthlyPrice === "number" && (
<div key={index} className="flex justify-between text-sm">
<span className="text-gray-600">{addon.name}:</span>
<span className="font-medium">¥{addon.price.toLocaleString()}/month</span>
<span className="font-medium">
¥{addon.monthlyPrice.toLocaleString()}/month
</span>
</div>
)
)}
@ -143,17 +148,21 @@ export function OrderSummary({
{activationFees.map((fee, index) => (
<div key={index} className="flex justify-between text-sm">
<span className="text-gray-600">{fee.name}:</span>
<span className="font-medium">¥{fee.price?.toLocaleString() || 0}</span>
<span className="font-medium">
¥{(fee.oneTimePrice ?? fee.monthlyPrice ?? 0).toLocaleString()}
</span>
</div>
))}
{selectedAddons.map(
(addon, index) =>
addon.billingCycle !== "Monthly" &&
addon.price && (
typeof addon.oneTimePrice === "number" && (
<div key={index} className="flex justify-between text-sm">
<span className="text-gray-600">{addon.name}:</span>
<span className="font-medium">¥{addon.price.toLocaleString()}</span>
<span className="font-medium">
¥{addon.oneTimePrice.toLocaleString()}
</span>
</div>
)
)}
@ -185,7 +194,9 @@ export function OrderSummary({
{activationFees.map((fee, index) => (
<div key={index} className="flex justify-between">
<span className="text-gray-600">{fee.name}</span>
<span className="font-medium">¥{fee.price?.toLocaleString()} one-time</span>
<span className="font-medium">
¥{(fee.oneTimePrice ?? fee.monthlyPrice ?? 0).toLocaleString()} one-time
</span>
</div>
))}
@ -194,7 +205,10 @@ export function OrderSummary({
<div key={index} className="flex justify-between">
<span className="text-gray-600">{addon.name}</span>
<span className="font-medium">
¥{addon.price?.toLocaleString()}
¥{(addon.billingCycle === "Monthly"
? addon.monthlyPrice ?? addon.oneTimePrice ?? 0
: addon.oneTimePrice ?? addon.monthlyPrice ?? 0
).toLocaleString()}
{addon.billingCycle === "Monthly" ? "/mo" : " one-time"}
</span>
</div>

View File

@ -3,12 +3,12 @@
import { AnimatedCard } from "@/components/ui";
import { Button } from "@/components/ui/button";
import { CurrencyYenIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import type { InternetProduct } from "@customer-portal/domain";
import type { InternetPlan, InternetInstallation } from "@/shared/types/catalog.types";
import { getMonthlyPrice, getOneTimePrice } from "../../utils/pricing";
interface InternetPlanCardProps {
plan: InternetProduct;
installations: InternetProduct[];
plan: InternetPlan;
installations: InternetInstallation[];
disabled?: boolean;
disabledReason?: string;
}
@ -19,14 +19,16 @@ export function InternetPlanCard({ plan, installations, disabled, disabledReason
const isPlatinum = tier === "Platinum";
const isSilver = tier === "Silver";
const minInstallationPrice = installations.length
? Math.min(
...installations.map(installation =>
installation.billingCycle === "Monthly"
? getMonthlyPrice(installation)
: getOneTimePrice(installation)
)
)
const installationPrices = installations
.map(installation =>
installation.billingCycle === "Monthly"
? getMonthlyPrice(installation)
: getOneTimePrice(installation)
)
.filter(price => price > 0);
const minInstallationPrice = installationPrices.length
? Math.min(...installationPrices)
: 0;
const getBorderClass = () => {
@ -75,13 +77,15 @@ export function InternetPlanCard({ plan, installations, disabled, disabledReason
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">{plan.name}</h3>
<p className="text-gray-600 text-sm mb-4">{plan.tierDescription || plan.description}</p>
<p className="text-gray-600 text-sm mb-4">
{plan.catalogMetadata.tierDescription || plan.description}
</p>
<div className="mb-6 flex-grow">
<h4 className="font-medium text-gray-900 mb-3">Your Plan Includes:</h4>
<ul className="space-y-2 text-sm text-gray-700">
{plan.features && plan.features.length > 0 ? (
plan.features.map((feature, index) => (
{plan.catalogMetadata.features && plan.catalogMetadata.features.length > 0 ? (
plan.catalogMetadata.features.map((feature, index) => (
<li key={index} className="flex items-start">
<span className="text-green-600 mr-2"></span>
{feature}

View File

@ -1,86 +1,52 @@
/**
* Frontend catalog types - Clean, focused, production-ready
* NO NAME MATCHING - Direct product access only
* NO HARDCODED DATA - Only structured Salesforce data
*/
import type {
InternetProduct,
SimProduct,
VpnProduct,
ProductWithPricing,
} from "@customer-portal/domain";
// Core product interface
export interface BaseProduct {
id: string;
name: string;
sku: string;
description: string;
}
// API shapes returned by the catalog endpoints. We model them as extensions of the
// unified domain types so field names stay aligned with Salesforce structures.
export type InternetPlan = InternetProduct & {
catalogMetadata: {
tierDescription: string;
features: string[];
isRecommended: boolean;
};
};
// Internet product types
export interface InternetPlan extends BaseProduct {
tier: "Silver" | "Gold" | "Platinum";
offeringType: string;
monthlyPrice?: number;
features: string[];
tierDescription: string;
isRecommended: boolean;
}
export type InternetInstallation = InternetProduct & {
catalogMetadata: {
installationTerm: "One-time" | "12-Month" | "24-Month";
};
};
export interface InternetInstallation extends BaseProduct {
type: "One-time" | "12-Month" | "24-Month";
price?: number;
billingCycle: string;
displayOrder: number;
}
export type InternetAddon = InternetProduct & {
catalogMetadata: {
addonCategory: "hikari-denwa-service" | "hikari-denwa-installation" | "other";
autoAdd: boolean;
requiredWith: string[];
};
};
export interface InternetAddon extends BaseProduct {
type: "hikari-denwa-service" | "hikari-denwa-installation" | "other";
monthlyPrice?: number;
activationPrice?: number;
autoAdd: boolean;
requiredWith: string[];
bundledAddonId?: string; // ID of the bundled product
isBundledAddon?: boolean; // Whether this is part of a bundle
displayOrder?: number;
}
export type SimPlan = SimProduct;
// SIM product types
export interface SimPlan extends BaseProduct {
dataSize: string;
planType: "DataOnly" | "DataSmsVoice" | "VoiceOnly";
hasFamilyDiscount: boolean;
monthlyPrice?: number;
features: string[];
}
export type SimActivationFee = SimProduct & {
catalogMetadata: {
isDefault: boolean;
};
};
export interface SimActivationFee extends BaseProduct {
price: number;
isDefault: boolean;
}
export type SimAddon = SimProduct;
export interface SimAddon extends BaseProduct {
price: number;
billingCycle: string;
bundledAddonId?: string; // ID of the bundled product
isBundledAddon?: boolean; // Whether this is part of a bundle
displayOrder?: number;
}
export type VpnPlan = VpnProduct;
export type VpnActivationFee = VpnProduct;
// VPN product types
export interface VpnPlan extends BaseProduct {
region: string; // VPN region from VPN_Region__c (USA-SF, UK-London, Global)
monthlyPrice?: number;
}
export interface VpnActivationFee extends BaseProduct {
price: number;
region?: string;
isDefault: boolean;
}
// Helper types
export type ProductType = "Internet" | "SIM" | "VPN";
export type ItemClass = "Service" | "Installation" | "Add-on" | "Activation";
export type BillingCycle = "Monthly" | "Onetime";
export type PlanTier = "Silver" | "Gold" | "Platinum";
// Frontend-specific types
export interface CheckoutState {
loading: boolean;
error: string | null;
@ -102,7 +68,6 @@ export interface OrderTotals {
oneTimeTotal: number;
}
// Production-ready utility functions - only structured Salesforce data
export const buildInternetOrderItems = (
plan: InternetPlan,
addons: InternetAddon[],
@ -114,29 +79,27 @@ export const buildInternetOrderItems = (
): OrderItem[] => {
const items: OrderItem[] = [];
// Add main plan
items.push({
name: plan.name,
sku: plan.sku,
monthlyPrice: plan.monthlyPrice,
oneTimePrice: plan.oneTimePrice,
type: "service",
});
// Add installation if selected (by SKU)
if (selections.installationSku) {
const installation = installations.find(inst => inst.sku === selections.installationSku);
if (installation) {
items.push({
name: installation.name,
sku: installation.sku,
monthlyPrice: installation.billingCycle === "Monthly" ? installation.price : undefined,
oneTimePrice: installation.billingCycle !== "Monthly" ? installation.price : undefined,
monthlyPrice: installation.billingCycle === "Monthly" ? installation.monthlyPrice : undefined,
oneTimePrice: installation.billingCycle !== "Monthly" ? installation.oneTimePrice : undefined,
type: "installation",
});
}
}
// Add selected addons (by SKU like SIM flow)
if (selections.addonSkus && selections.addonSkus.length > 0) {
selections.addonSkus.forEach(addonSku => {
const selectedAddon = addons.find(addon => addon.sku === addonSku);
@ -145,7 +108,7 @@ export const buildInternetOrderItems = (
name: selectedAddon.name,
sku: selectedAddon.sku,
monthlyPrice: selectedAddon.monthlyPrice,
oneTimePrice: selectedAddon.activationPrice,
oneTimePrice: selectedAddon.oneTimePrice,
type: "addon",
});
}
@ -160,32 +123,29 @@ export const buildSimOrderItems = (
activationFees: SimActivationFee[],
addons: SimAddon[],
selections: {
addonSkus?: string[]; // Support multiple add-ons
addonSkus?: string[];
}
): OrderItem[] => {
const items: OrderItem[] = [];
// Add main plan
items.push({
name: plan.name,
sku: plan.sku, // This should be the actual StockKeepingUnit from Salesforce
sku: plan.sku,
monthlyPrice: plan.monthlyPrice,
oneTimePrice: plan.oneTimePrice,
type: "service",
});
// Add activation fee
const activationFee = activationFees[0];
const activationFee = activationFees.find(fee => fee.catalogMetadata.isDefault) || activationFees[0];
if (activationFee) {
items.push({
name: activationFee.name,
sku: activationFee.sku,
oneTimePrice: activationFee.price,
oneTimePrice: activationFee.oneTimePrice ?? activationFee.unitPrice,
type: "addon",
});
}
// Add selected addons from Salesforce (multiple allowed)
if (selections.addonSkus && selections.addonSkus.length > 0) {
selections.addonSkus.forEach(addonSku => {
const selectedAddon = addons.find(addon => addon.sku === addonSku);
@ -193,12 +153,10 @@ export const buildSimOrderItems = (
items.push({
name: selectedAddon.name,
sku: selectedAddon.sku,
monthlyPrice: selectedAddon.billingCycle === "Monthly" ? selectedAddon.price : undefined,
oneTimePrice: selectedAddon.billingCycle !== "Monthly" ? selectedAddon.price : undefined,
monthlyPrice: selectedAddon.billingCycle === "Monthly" ? selectedAddon.monthlyPrice ?? selectedAddon.unitPrice : undefined,
oneTimePrice: selectedAddon.billingCycle !== "Monthly" ? selectedAddon.oneTimePrice ?? selectedAddon.unitPrice : undefined,
type: "addon",
});
} else {
// Addon SKU not found - skip silently in production
}
});
}

View File

@ -13,7 +13,7 @@ import {
} from "@heroicons/react/24/outline";
import { useInternetCatalog } from "@/features/catalog/hooks";
import { useActiveSubscriptions } from "@/features/subscriptions/hooks/useSubscriptions";
import type { InternetProduct } from "@customer-portal/domain";
import type { InternetPlan, InternetInstallation } from "@/shared/types/catalog.types";
import { getMonthlyPrice } from "../utils/pricing";
import { LoadingCard, Skeleton, LoadingTable } from "@/components/ui/loading-skeleton";
import { AnimatedCard } from "@/components/ui";
@ -24,8 +24,8 @@ import { AlertBanner } from "@/components/common/AlertBanner";
export function InternetPlansContainer() {
const { data, isLoading, error } = useInternetCatalog();
const plans: InternetProduct[] = data?.plans || [];
const installations: InternetProduct[] = data?.installations || [];
const plans: InternetPlan[] = data?.plans || [];
const installations: InternetInstallation[] = data?.installations || [];
const [eligibility, setEligibility] = useState<string>("");
const { data: activeSubs } = useActiveSubscriptions();
const hasActiveInternet = Array.isArray(activeSubs)
@ -44,16 +44,16 @@ export function InternetPlansContainer() {
}
}, [plans]);
const getEligibilityIcon = (offeringType: string) => {
const lower = offeringType.toLowerCase();
const getEligibilityIcon = (offeringType?: string) => {
const lower = (offeringType || "").toLowerCase();
if (lower.includes("home")) return <HomeIcon className="h-5 w-5" />;
if (lower.includes("apartment"))
return <BuildingOfficeIcon className="h-5 w-5" />;
return <HomeIcon className="h-5 w-5" />;
};
const getEligibilityColor = (offeringType: string) => {
const lower = offeringType.toLowerCase();
const getEligibilityColor = (offeringType?: string) => {
const lower = (offeringType || "").toLowerCase();
if (lower.includes("home"))
return "text-blue-600 bg-blue-50 border-blue-200";
if (lower.includes("apartment"))
@ -186,4 +186,3 @@ export function InternetPlansContainer() {
}
// InternetPlanCard extracted to components/internet/InternetPlanCard

View File

@ -112,12 +112,6 @@ export interface ProductWithPricing extends BaseProduct {
unitPrice?: number; // PricebookEntry.UnitPrice
monthlyPrice?: number; // UnitPrice if billingCycle === "Monthly"
oneTimePrice?: number; // UnitPrice if billingCycle === "Onetime"
/** @deprecated Legacy field kept temporarily for UI compatibility. Use monthlyPrice/oneTimePrice instead. */
price?: number;
/** @deprecated Legacy field kept temporarily for UI compatibility. Use oneTimePrice instead. */
activationPrice?: number;
/** @deprecated Legacy field kept temporarily for UI compatibility. Prefer deriving type from sku/itemClass. */
type?: string;
}
// Internet-specific product fields
@ -133,10 +127,6 @@ export interface InternetProduct extends ProductWithPricing {
features?: string[];
tierDescription?: string;
isRecommended?: boolean;
/** @deprecated Use internetPlanTier. */
tier?: InternetProduct["internetPlanTier"];
/** @deprecated Use internetOfferingType. */
offeringType?: string;
}
// SIM-specific product fields
@ -150,12 +140,6 @@ export interface SimProduct extends ProductWithPricing {
// UI-specific fields
features?: string[];
/** @deprecated Use simDataSize. */
dataSize?: string;
/** @deprecated Use simPlanType. */
planType?: SimProduct["simPlanType"];
/** @deprecated Use simHasFamilyDiscount. */
hasFamilyDiscount?: boolean;
}
// VPN-specific product fields
@ -164,8 +148,6 @@ export interface VpnProduct extends ProductWithPricing {
// VPN-specific fields
vpnRegion?: string; // VPN_Region__c
/** @deprecated Use vpnRegion. */
region?: string;
}
// Generic product for "Other" category
@ -339,10 +321,6 @@ export function fromSalesforceProduct2(
| InternetProduct["internetPlanTier"]
| undefined,
internetOfferingType: coerceString(readField(sfProduct, fieldMap.internetOfferingType)) || undefined,
tier: coerceString(readField(sfProduct, fieldMap.internetPlanTier)) as
| InternetProduct["internetPlanTier"]
| undefined,
offeringType: coerceString(readField(sfProduct, fieldMap.internetOfferingType)) || undefined,
} satisfies InternetProduct;
case "SIM":
@ -351,16 +329,12 @@ export function fromSalesforceProduct2(
simDataSize: coerceString(readField(sfProduct, fieldMap.simDataSize)) || undefined,
simPlanType: normalizeSimPlanType(readField(sfProduct, fieldMap.simPlanType)) || undefined,
simHasFamilyDiscount: normalizeBoolean(readField(sfProduct, fieldMap.simHasFamilyDiscount)),
dataSize: coerceString(readField(sfProduct, fieldMap.simDataSize)) || undefined,
planType: normalizeSimPlanType(readField(sfProduct, fieldMap.simPlanType)) || undefined,
hasFamilyDiscount: normalizeBoolean(readField(sfProduct, fieldMap.simHasFamilyDiscount)) ?? undefined,
} satisfies SimProduct;
case "VPN":
return {
...baseProduct,
vpnRegion: coerceString(readField(sfProduct, fieldMap.vpnRegion)) || undefined,
region: coerceString(readField(sfProduct, fieldMap.vpnRegion)) || undefined,
} satisfies VpnProduct;
default: