Refactor Salesforce integration by removing the SalesforceFieldMapService from various modules and updating import paths to utilize direct field references. This change enhances type safety and maintainability while streamlining data handling in order-related services and catalog modules.

This commit is contained in:
barsa 2025-10-08 11:22:01 +09:00
parent 88d58f9ac5
commit e5c5f352f2
15 changed files with 129 additions and 740 deletions

View File

@ -4,7 +4,6 @@ import { QueueModule } from "@bff/core/queue/queue.module";
import { SalesforceService } from "./salesforce.service";
import { SalesforceConnection } from "./services/salesforce-connection.service";
import { SalesforceAccountService } from "./services/salesforce-account.service";
import { SalesforceFieldMapService } from "./services/salesforce-field-config.service";
@Module({
imports: [QueueModule, ConfigModule],
@ -12,8 +11,7 @@ import { SalesforceFieldMapService } from "./services/salesforce-field-config.se
SalesforceConnection,
SalesforceAccountService,
SalesforceService,
SalesforceFieldMapService,
],
exports: [SalesforceService, SalesforceConnection, SalesforceFieldMapService],
exports: [SalesforceService, SalesforceConnection],
})
export class SalesforceModule {}

View File

@ -3,7 +3,6 @@ import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config";
import { getErrorMessage } from "@bff/core/utils/error.util";
import { SalesforceConnection } from "./services/salesforce-connection.service";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import {
SalesforceAccountService,
type AccountData,
@ -27,7 +26,6 @@ export class SalesforceService implements OnModuleInit {
private configService: ConfigService,
private connection: SalesforceConnection,
private accountService: SalesforceAccountService,
private fieldMapService: SalesforceFieldMapService,
@Inject(Logger) private readonly logger: Logger
) {}
@ -118,10 +116,9 @@ export class SalesforceService implements OnModuleInit {
throw new Error("Salesforce connection not available");
}
const fields = this.fieldMapService.getFieldMap();
const result = (await this.connection.query(
`SELECT Id, Status, ${fields.order.activationStatus}, ${fields.order.whmcsOrderId},
${fields.order.lastErrorCode}, ${fields.order.lastErrorMessage},
`SELECT Id, Status, Type, Activation_Status__c, WHMCS_Order_ID__c,
Activation_Error_Code__c, Activation_Error_Message__c,
AccountId, Account.Name
FROM Order
WHERE Id = '${orderId}'

View File

@ -1,152 +0,0 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import type { SalesforceProductFieldMap } from "@customer-portal/domain/catalog";
import {
buildOrderItemProduct2Fields,
buildOrderItemSelectFields,
buildOrderSelectFields,
type SalesforceOrdersFieldConfig,
} from "@customer-portal/domain/orders";
import type { SalesforceAccountFieldMap } from "@customer-portal/domain/customer";
/**
* Salesforce field configuration service
* Provides the mapping between logical field names and actual Salesforce custom field names
*/
export type SalesforceFieldConfig = SalesforceOrdersFieldConfig;
// Legacy alias for backwards compatibility
export type SalesforceFieldMap = SalesforceFieldConfig;
@Injectable()
export class SalesforceFieldMapService {
constructor(private readonly configService: ConfigService) {}
getFieldMap(): SalesforceFieldConfig {
const product: SalesforceProductFieldMap = {
sku: this.configService.get<string>("PRODUCT_SKU_FIELD")!,
portalCategory: this.configService.get<string>("PRODUCT_PORTAL_CATEGORY_FIELD")!,
portalCatalog: this.configService.get<string>("PRODUCT_PORTAL_CATALOG_FIELD")!,
portalAccessible: this.configService.get<string>("PRODUCT_PORTAL_ACCESSIBLE_FIELD")!,
itemClass: this.configService.get<string>("PRODUCT_ITEM_CLASS_FIELD")!,
billingCycle: this.configService.get<string>("PRODUCT_BILLING_CYCLE_FIELD")!,
whmcsProductId: this.configService.get<string>("PRODUCT_WHMCS_PRODUCT_ID_FIELD")!,
whmcsProductName: this.configService.get<string>("PRODUCT_WHMCS_PRODUCT_NAME_FIELD")!,
internetPlanTier: this.configService.get<string>("PRODUCT_INTERNET_PLAN_TIER_FIELD")!,
internetOfferingType: this.configService.get<string>("PRODUCT_INTERNET_OFFERING_TYPE_FIELD")!,
displayOrder: this.configService.get<string>("PRODUCT_DISPLAY_ORDER_FIELD")!,
bundledAddon: this.configService.get<string>("PRODUCT_BUNDLED_ADDON_FIELD")!,
isBundledAddon: this.configService.get<string>("PRODUCT_IS_BUNDLED_ADDON_FIELD")!,
simDataSize: this.configService.get<string>("PRODUCT_SIM_DATA_SIZE_FIELD")!,
simPlanType: this.configService.get<string>("PRODUCT_SIM_PLAN_TYPE_FIELD")!,
simHasFamilyDiscount: this.configService.get<string>("PRODUCT_SIM_HAS_FAMILY_DISCOUNT_FIELD")!,
vpnRegion: this.configService.get<string>("PRODUCT_VPN_REGION_FIELD")!,
};
const fieldConfig: SalesforceFieldConfig = {
account: {
internetEligibility: this.configService.get<string>("ACCOUNT_INTERNET_ELIGIBILITY_FIELD")!,
customerNumber: this.configService.get<string>("ACCOUNT_CUSTOMER_NUMBER_FIELD")!,
},
product,
order: {
orderType: this.configService.get<string>("ORDER_TYPE_FIELD")!,
activationType: this.configService.get<string>("ORDER_ACTIVATION_TYPE_FIELD")!,
activationScheduledAt: this.configService.get<string>(
"ORDER_ACTIVATION_SCHEDULED_AT_FIELD"
)!,
activationStatus: this.configService.get<string>("ORDER_ACTIVATION_STATUS_FIELD")!,
internetPlanTier: this.configService.get<string>("ORDER_INTERNET_PLAN_TIER_FIELD")!,
installationType: this.configService.get<string>("ORDER_INSTALLATION_TYPE_FIELD")!,
weekendInstall: this.configService.get<string>("ORDER_WEEKEND_INSTALL_FIELD")!,
accessMode: this.configService.get<string>("ORDER_ACCESS_MODE_FIELD")!,
hikariDenwa: this.configService.get<string>("ORDER_HIKARI_DENWA_FIELD")!,
vpnRegion: this.configService.get<string>("ORDER_VPN_REGION_FIELD")!,
simType: this.configService.get<string>("ORDER_SIM_TYPE_FIELD")!,
eid: this.configService.get<string>("ORDER_EID_FIELD")!,
simVoiceMail: this.configService.get<string>("ORDER_SIM_VOICE_MAIL_FIELD")!,
simCallWaiting: this.configService.get<string>("ORDER_SIM_CALL_WAITING_FIELD")!,
mnp: {
application: this.configService.get<string>("ORDER_MNP_APPLICATION_FIELD")!,
reservationNumber: this.configService.get<string>("ORDER_MNP_RESERVATION_FIELD")!,
expiryDate: this.configService.get<string>("ORDER_MNP_EXPIRY_FIELD")!,
phoneNumber: this.configService.get<string>("ORDER_MNP_PHONE_FIELD")!,
mvnoAccountNumber: this.configService.get<string>("ORDER_MVNO_ACCOUNT_NUMBER_FIELD")!,
portingDateOfBirth: this.configService.get<string>("ORDER_PORTING_DOB_FIELD")!,
portingFirstName: this.configService.get<string>("ORDER_PORTING_FIRST_NAME_FIELD")!,
portingLastName: this.configService.get<string>("ORDER_PORTING_LAST_NAME_FIELD")!,
portingFirstNameKatakana: this.configService.get<string>(
"ORDER_PORTING_FIRST_NAME_KATAKANA_FIELD"
)!,
portingLastNameKatakana: this.configService.get<string>(
"ORDER_PORTING_LAST_NAME_KATAKANA_FIELD"
)!,
portingGender: this.configService.get<string>("ORDER_PORTING_GENDER_FIELD")!,
},
whmcsOrderId: this.configService.get<string>("ORDER_WHMCS_ORDER_ID_FIELD")!,
lastErrorCode: this.configService.get<string>("ORDER_ACTIVATION_ERROR_CODE_FIELD"),
lastErrorMessage: this.configService.get<string>("ORDER_ACTIVATION_ERROR_MESSAGE_FIELD"),
lastAttemptAt: this.configService.get<string>("ORDER_ACTIVATION_LAST_ATTEMPT_AT_FIELD"),
addressChanged: this.configService.get<string>("ORDER_ADDRESS_CHANGED_FIELD")!,
billing: {
street: this.configService.get<string>("ORDER_BILLING_STREET_FIELD")!,
city: this.configService.get<string>("ORDER_BILLING_CITY_FIELD")!,
state: this.configService.get<string>("ORDER_BILLING_STATE_FIELD")!,
postalCode: this.configService.get<string>("ORDER_BILLING_POSTAL_CODE_FIELD")!,
country: this.configService.get<string>("ORDER_BILLING_COUNTRY_FIELD")!,
},
},
orderItem: {
billingCycle: this.configService.get<string>("ORDER_ITEM_BILLING_CYCLE_FIELD")!,
whmcsServiceId: this.configService.get<string>("ORDER_ITEM_WHMCS_SERVICE_ID_FIELD")!,
},
};
return fieldConfig;
}
getProductQueryFields(): string {
const fields = this.getFieldMap();
return [
"Id",
"Name",
fields.product.sku,
fields.product.portalCategory,
fields.product.portalCatalog,
fields.product.portalAccessible,
fields.product.itemClass,
fields.product.billingCycle,
fields.product.whmcsProductId,
fields.product.whmcsProductName,
fields.product.internetPlanTier,
fields.product.internetOfferingType,
fields.product.displayOrder,
fields.product.bundledAddon,
fields.product.isBundledAddon,
fields.product.simDataSize,
fields.product.simPlanType,
fields.product.simHasFamilyDiscount,
fields.product.vpnRegion,
"UnitPrice",
"IsActive",
].join(", ");
}
getOrderQueryFields(): string {
const fieldConfig = this.getFieldMap();
const fields = buildOrderSelectFields(fieldConfig, ["Account.Name"]);
return fields.join(", ");
}
getOrderItemQueryFields(additional: string[] = []): string {
const fieldConfig = this.getFieldMap();
const fields = buildOrderItemSelectFields(fieldConfig, additional);
return fields.join(", ");
}
getOrderItemProduct2Select(additional: string[] = []): string {
const fieldConfig = this.getFieldMap();
const productFields = buildOrderItemProduct2Fields(fieldConfig, additional);
return productFields.map(f => `PricebookEntry.Product2.${f}`).join(", ");
}
}

View File

@ -2,7 +2,6 @@ import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import {
assertSalesforceId,
sanitizeSoqlLiteral,
@ -12,6 +11,7 @@ import type {
SalesforceProduct2WithPricebookEntries,
SalesforcePricebookEntryRecord,
} from "@customer-portal/domain/catalog";
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
import type { SalesforceQueryResult } from "@customer-portal/domain/orders";
@Injectable()
@ -20,7 +20,6 @@ export class BaseCatalogService {
constructor(
protected readonly sf: SalesforceConnection,
protected readonly fieldMapService: SalesforceFieldMapService,
private readonly configService: ConfigService,
@Inject(Logger) protected readonly logger: Logger
) {
@ -28,10 +27,6 @@ export class BaseCatalogService {
this.portalPriceBookId = assertSalesforceId(portalPricebook, "PORTAL_PRICEBOOK_ID");
}
protected getFields() {
return this.fieldMapService.getFieldMap();
}
protected async executeQuery<TRecord extends SalesforceProduct2WithPricebookEntries>(
soql: string,
context: string
@ -52,13 +47,9 @@ export class BaseCatalogService {
protected extractPricebookEntry(
record: SalesforceProduct2WithPricebookEntries
): SalesforcePricebookEntryRecord | undefined {
const pricebookEntries = record.PricebookEntries?.records;
const entry = Array.isArray(pricebookEntries) ? pricebookEntries[0] : undefined;
const entry = CatalogProviders.Salesforce.extractPricebookEntry(record);
if (!entry) {
const fields = this.getFields();
const skuField = fields.product.sku;
const skuRaw = Reflect.get(record, skuField) as unknown;
const sku = typeof skuRaw === "string" ? skuRaw : undefined;
const sku = record.StockKeepingUnit ?? undefined;
this.logger.warn(
`No pricebook entry found for product ${String(record.Name)} (SKU: ${String(sku ?? "")}). Pricebook ID: ${this.portalPriceBookId}.`
);
@ -74,13 +65,12 @@ export class BaseCatalogService {
additionalFields: string[] = [],
additionalConditions: string = ""
): string {
const fields = this.getFields();
const baseFields = [
"Id",
"Name",
fields.product.sku,
fields.product.portalCategory,
fields.product.itemClass,
"StockKeepingUnit",
"Portal_Category__c",
"Item_Class__c",
];
const allFields = [...baseFields, ...additionalFields].join(", ");
@ -91,11 +81,11 @@ export class BaseCatalogService {
SELECT ${allFields},
(SELECT Id, UnitPrice, Pricebook2Id, Product2Id, IsActive FROM PricebookEntries WHERE Pricebook2Id = '${this.portalPriceBookId}' AND IsActive = true LIMIT 1)
FROM Product2
WHERE ${fields.product.portalCategory} = '${safeCategory}'
AND ${fields.product.itemClass} = '${safeItemClass}'
AND ${fields.product.portalAccessible} = true
WHERE Portal_Category__c = '${safeCategory}'
AND Item_Class__c = '${safeItemClass}'
AND Portal_Accessible__c = true
${additionalConditions}
ORDER BY ${fields.product.displayOrder} NULLS LAST, Name
ORDER BY Catalog_Order__c NULLS LAST, Name
`;
}
@ -114,12 +104,11 @@ export class BaseCatalogService {
}
protected buildCatalogServiceQuery(category: string, additionalFields: string[] = []): string {
const fields = this.getFields();
return this.buildProductQuery(
category,
"Service",
additionalFields,
`AND ${fields.product.portalCatalog} = true`
`AND Portal_Catalog__c = true`
);
}
}

View File

@ -7,17 +7,12 @@ import type {
InternetInstallationCatalogItem,
InternetAddonCatalogItem,
} from "@customer-portal/domain/catalog";
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util";
import { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util";
import {
mapInternetPlan,
mapInternetInstallation,
mapInternetAddon,
} from "@bff/modules/catalog/utils/salesforce-product.mapper";
interface SalesforceAccount {
Id: string;
@ -28,20 +23,18 @@ interface SalesforceAccount {
export class InternetCatalogService extends BaseCatalogService {
constructor(
sf: SalesforceConnection,
fieldMapService: SalesforceFieldMapService,
configService: ConfigService,
@Inject(Logger) logger: Logger,
private mappingsService: MappingsService
) {
super(sf, fieldMapService, configService, logger);
super(sf, configService, logger);
}
async getPlans(): Promise<InternetPlanCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildCatalogServiceQuery("Internet", [
fields.product.internetPlanTier,
fields.product.internetOfferingType,
fields.product.displayOrder,
"Internet_Plan_Tier__c",
"Internet_Offering_Type__c",
"Catalog_Order__c",
]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
@ -50,15 +43,14 @@ export class InternetCatalogService extends BaseCatalogService {
return records.map(record => {
const entry = this.extractPricebookEntry(record);
return mapInternetPlan(record, fields, entry);
return CatalogProviders.Salesforce.mapInternetPlan(record, entry);
});
}
async getInstallations(): Promise<InternetInstallationCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("Internet", "Installation", [
fields.product.billingCycle,
fields.product.displayOrder,
"Billing_Cycle__c",
"Catalog_Order__c",
]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
@ -70,18 +62,17 @@ export class InternetCatalogService extends BaseCatalogService {
return records
.map(record => {
const entry = this.extractPricebookEntry(record);
return mapInternetInstallation(record, fields, entry);
return CatalogProviders.Salesforce.mapInternetInstallation(record, entry);
})
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
}
async getAddons(): Promise<InternetAddonCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("Internet", "Add-on", [
fields.product.billingCycle,
fields.product.displayOrder,
fields.product.bundledAddon,
fields.product.isBundledAddon,
"Billing_Cycle__c",
"Catalog_Order__c",
"Bundled_Addon__c",
"Is_Bundled_Addon__c",
]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
@ -93,7 +84,7 @@ export class InternetCatalogService extends BaseCatalogService {
return records
.map(record => {
const entry = this.extractPricebookEntry(record);
return mapInternetAddon(record, fields, entry);
return CatalogProviders.Salesforce.mapInternetAddon(record, entry);
})
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
}
@ -120,9 +111,8 @@ export class InternetCatalogService extends BaseCatalogService {
}
// Get customer's eligibility from Salesforce
const fields = this.getFields();
const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");
const soql = `SELECT Id, ${fields.account.internetEligibility} FROM Account WHERE Id = '${sfAccountId}' LIMIT 1`;
const soql = `SELECT Id, Internet_Eligibility__c FROM Account WHERE Id = '${sfAccountId}' LIMIT 1`;
const accounts = await this.executeQuery(soql, "Customer Eligibility");
if (accounts.length === 0) {

View File

@ -1,16 +1,12 @@
import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { BaseCatalogService } from "./base-catalog.service";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import type {
SalesforceProduct2WithPricebookEntries,
SimCatalogProduct,
SimActivationFeeCatalogItem,
} from "@customer-portal/domain/catalog";
import {
mapSimProduct,
mapSimActivationFee,
} from "@bff/modules/catalog/utils/salesforce-product.mapper";
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
import { Logger } from "nestjs-pino";
@ -20,22 +16,20 @@ import { WhmcsConnectionOrchestratorService } from "@bff/integrations/whmcs/conn
export class SimCatalogService extends BaseCatalogService {
constructor(
sf: SalesforceConnection,
fieldMapService: SalesforceFieldMapService,
configService: ConfigService,
@Inject(Logger) logger: Logger,
private mappingsService: MappingsService,
private whmcs: WhmcsConnectionOrchestratorService
) {
super(sf, fieldMapService, configService, logger);
super(sf, configService, logger);
}
async getPlans(): Promise<SimCatalogProduct[]> {
const fields = this.getFields();
const soql = this.buildCatalogServiceQuery("SIM", [
fields.product.simDataSize,
fields.product.simPlanType,
fields.product.simHasFamilyDiscount,
fields.product.displayOrder,
"SIM_Data_Size__c",
"SIM_Plan_Type__c",
"SIM_Has_Family_Discount__c",
"Catalog_Order__c",
]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
@ -44,7 +38,7 @@ export class SimCatalogService extends BaseCatalogService {
return records.map(record => {
const entry = this.extractPricebookEntry(record);
const product = mapSimProduct(record, fields, entry);
const product = CatalogProviders.Salesforce.mapSimProduct(record, entry);
return {
...product,
@ -54,7 +48,6 @@ export class SimCatalogService extends BaseCatalogService {
}
async getActivationFees(): Promise<SimActivationFeeCatalogItem[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("SIM", "Activation", []);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
@ -63,17 +56,16 @@ export class SimCatalogService extends BaseCatalogService {
return records.map(record => {
const entry = this.extractPricebookEntry(record);
return mapSimActivationFee(record, fields, entry);
return CatalogProviders.Salesforce.mapSimActivationFee(record, entry);
});
}
async getAddons(): Promise<SimCatalogProduct[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("SIM", "Add-on", [
fields.product.billingCycle,
fields.product.displayOrder,
fields.product.bundledAddon,
fields.product.isBundledAddon,
"Billing_Cycle__c",
"Catalog_Order__c",
"Bundled_Addon__c",
"Is_Bundled_Addon__c",
]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
@ -83,7 +75,7 @@ export class SimCatalogService extends BaseCatalogService {
return records
.map(record => {
const entry = this.extractPricebookEntry(record);
const product = mapSimProduct(record, fields, entry);
const product = CatalogProviders.Salesforce.mapSimProduct(record, entry);
return {
...product,

View File

@ -2,26 +2,23 @@ import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import { BaseCatalogService } from "./base-catalog.service";
import type { SalesforceProduct2WithPricebookEntries, VpnCatalogProduct } from "@customer-portal/domain/catalog";
import { mapVpnProduct } from "@bff/modules/catalog/utils/salesforce-product.mapper";
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
@Injectable()
export class VpnCatalogService extends BaseCatalogService {
constructor(
sf: SalesforceConnection,
fieldMapService: SalesforceFieldMapService,
configService: ConfigService,
@Inject(Logger) logger: Logger
) {
super(sf, fieldMapService, configService, logger);
super(sf, configService, logger);
}
async getPlans(): Promise<VpnCatalogProduct[]> {
const fields = this.getFields();
const soql = this.buildCatalogServiceQuery("VPN", [
fields.product.vpnRegion,
fields.product.displayOrder,
"VPN_Region__c",
"Catalog_Order__c",
]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
@ -30,7 +27,7 @@ export class VpnCatalogService extends BaseCatalogService {
return records.map(record => {
const entry = this.extractPricebookEntry(record);
const product = mapVpnProduct(record, fields, entry);
const product = CatalogProviders.Salesforce.mapVpnProduct(record, entry);
return {
...product,
description: product.description || product.name,
@ -39,8 +36,7 @@ export class VpnCatalogService extends BaseCatalogService {
}
async getActivationFees(): Promise<VpnCatalogProduct[]> {
const fields = this.getFields();
const soql = this.buildProductQuery("VPN", "Activation", [fields.product.vpnRegion]);
const soql = this.buildProductQuery("VPN", "Activation", ["VPN_Region__c"]);
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
soql,
"VPN Activation Fees"
@ -48,7 +44,7 @@ export class VpnCatalogService extends BaseCatalogService {
return records.map(record => {
const pricebookEntry = this.extractPricebookEntry(record);
const product = mapVpnProduct(record, fields, pricebookEntry);
const product = CatalogProviders.Salesforce.mapVpnProduct(record, pricebookEntry);
return {
...product,

View File

@ -1,302 +0,0 @@
import type {
CatalogProductBase,
InternetAddonCatalogItem,
InternetInstallationCatalogItem,
InternetPlanCatalogItem,
InternetPlanTemplate,
SimActivationFeeCatalogItem,
SimCatalogProduct,
VpnCatalogProduct,
} from "@customer-portal/domain/catalog";
import type {
SalesforceProduct2WithPricebookEntries,
SalesforcePricebookEntryRecord,
} from "@customer-portal/domain/catalog";
import type { SalesforceFieldMap } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
export type SalesforceCatalogProductRecord = SalesforceProduct2WithPricebookEntries;
const DEFAULT_PLAN_TEMPLATE: InternetPlanTemplate = {
tierDescription: "Standard plan",
description: undefined,
features: undefined,
};
function getTierTemplate(tier?: string): InternetPlanTemplate {
if (!tier) {
return DEFAULT_PLAN_TEMPLATE;
}
const normalized = tier.toLowerCase();
switch (normalized) {
case "silver":
return {
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",
],
};
case "gold":
return {
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",
],
};
case "platinum":
return {
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",
],
};
default:
return {
tierDescription: `${tier} plan`,
description: undefined,
features: undefined,
};
}
}
function inferInstallationTypeFromSku(sku: string): "One-time" | "12-Month" | "24-Month" {
const normalized = sku.toLowerCase();
if (normalized.includes("24")) return "24-Month";
if (normalized.includes("12")) return "12-Month";
return "One-time";
}
function getProductField<T = unknown>(
product: SalesforceCatalogProductRecord,
fieldKey: keyof SalesforceFieldMap["product"],
fieldMap: SalesforceFieldMap
): T | undefined {
const salesforceField = fieldMap.product[fieldKey] as keyof SalesforceCatalogProductRecord;
const value = product[salesforceField];
return value as T | undefined;
}
export function getStringField(
product: SalesforceCatalogProductRecord,
fieldKey: keyof SalesforceFieldMap["product"],
fieldMap: SalesforceFieldMap
): string | undefined {
const value = getProductField(product, fieldKey, fieldMap);
return typeof value === "string" ? value : undefined;
}
function coerceNumber(value: unknown): number | undefined {
if (typeof value === "number") return value;
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}
function baseProduct(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap
): CatalogProductBase {
const sku = getStringField(product, "sku", fieldMap) ?? "";
const base: CatalogProductBase = {
id: product.Id,
sku,
name: product.Name ?? sku,
};
const description = product.Description;
if (description) base.description = description;
const billingCycle = getStringField(product, "billingCycle", fieldMap);
if (billingCycle) base.billingCycle = billingCycle;
const displayOrder = getProductField(product, "displayOrder", fieldMap);
if (typeof displayOrder === "number") base.displayOrder = displayOrder;
return base;
}
function getBoolean(
product: SalesforceCatalogProductRecord,
key: keyof SalesforceFieldMap["product"],
fieldMap: SalesforceFieldMap
) {
const value = getProductField(product, key, fieldMap);
return typeof value === "boolean" ? value : undefined;
}
function resolveBundledAddonId(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap
): string | undefined {
const raw = getProductField(product, "bundledAddon", fieldMap);
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
}
function resolveBundledAddon(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap
) {
return {
bundledAddonId: resolveBundledAddonId(product, fieldMap),
isBundledAddon: Boolean(getBoolean(product, "isBundledAddon", fieldMap)),
};
}
function derivePrices(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap,
pricebookEntry?: SalesforcePricebookEntryRecord
): Pick<CatalogProductBase, "monthlyPrice" | "oneTimePrice"> {
const billingCycle = getStringField(product, "billingCycle", fieldMap)?.toLowerCase();
const unitPrice = coerceNumber(pricebookEntry?.UnitPrice);
let monthlyPrice: number | undefined;
let oneTimePrice: number | undefined;
if (unitPrice !== undefined) {
if (billingCycle === "monthly") {
monthlyPrice = unitPrice;
} else if (billingCycle) {
oneTimePrice = unitPrice;
}
}
// Note: Monthly_Price__c and One_Time_Price__c fields would be used here if they exist in Salesforce
// For now, we rely on pricebook entries for pricing
return { monthlyPrice, oneTimePrice };
}
export function mapInternetPlan(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap,
pricebookEntry?: SalesforcePricebookEntryRecord
): InternetPlanCatalogItem {
const base = baseProduct(product, fieldMap);
const prices = derivePrices(product, fieldMap, pricebookEntry);
const tier = getStringField(product, "internetPlanTier", fieldMap);
const offeringType = getStringField(product, "internetOfferingType", fieldMap);
const tierData = getTierTemplate(tier);
return {
...base,
...prices,
internetPlanTier: tier,
internetOfferingType: offeringType,
features: tierData.features, // Use hardcoded tier features since no featureList field
catalogMetadata: {
tierDescription: tierData.tierDescription,
features: tierData.features,
isRecommended: tier === "Gold",
},
// Use Salesforce description if available, otherwise fall back to tier description
description: base.description ?? tierData.description,
};
}
export function mapInternetInstallation(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap,
pricebookEntry?: SalesforcePricebookEntryRecord
): InternetInstallationCatalogItem {
const base = baseProduct(product, fieldMap);
const prices = derivePrices(product, fieldMap, pricebookEntry);
return {
...base,
...prices,
catalogMetadata: {
installationTerm: inferInstallationTypeFromSku(base.sku),
},
};
}
export function mapInternetAddon(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap,
pricebookEntry?: SalesforcePricebookEntryRecord
): InternetAddonCatalogItem {
const base = baseProduct(product, fieldMap);
const prices = derivePrices(product, fieldMap, pricebookEntry);
const { bundledAddonId, isBundledAddon } = resolveBundledAddon(product, fieldMap);
return {
...base,
...prices,
bundledAddonId,
isBundledAddon,
};
}
export function mapSimProduct(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap,
pricebookEntry?: SalesforcePricebookEntryRecord
): SimCatalogProduct {
const base = baseProduct(product, fieldMap);
const prices = derivePrices(product, fieldMap, pricebookEntry);
const dataSize = getStringField(product, "simDataSize", fieldMap);
const planType = getStringField(product, "simPlanType", fieldMap);
const hasFamilyDiscount = getBoolean(product, "simHasFamilyDiscount", fieldMap);
const { bundledAddonId, isBundledAddon } = resolveBundledAddon(product, fieldMap);
return {
...base,
...prices,
simDataSize: dataSize,
simPlanType: planType,
simHasFamilyDiscount: hasFamilyDiscount,
bundledAddonId,
isBundledAddon,
};
}
export function mapSimActivationFee(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap,
pricebookEntry?: SalesforcePricebookEntryRecord
): SimActivationFeeCatalogItem {
const simProduct = mapSimProduct(product, fieldMap, pricebookEntry);
return {
...simProduct,
catalogMetadata: {
isDefault: true,
},
};
}
export function mapVpnProduct(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap,
pricebookEntry?: SalesforcePricebookEntryRecord
): VpnCatalogProduct {
const base = baseProduct(product, fieldMap);
const prices = derivePrices(product, fieldMap, pricebookEntry);
const vpnRegion = getStringField(product, "vpnRegion", fieldMap);
return {
...base,
...prices,
vpnRegion,
};
}

View File

@ -3,7 +3,6 @@ import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { OrderFulfillmentOrchestrator } from "../services/order-fulfillment-orchestrator.service";
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import type { ProvisioningJobData } from "./provisioning.queue";
import { CacheService } from "@bff/infra/cache/cache.service";
import { ConfigService } from "@nestjs/config";
@ -16,7 +15,6 @@ export class ProvisioningProcessor extends WorkerHost {
constructor(
private readonly orchestrator: OrderFulfillmentOrchestrator,
private readonly salesforceService: SalesforceService,
private readonly fieldMapService: SalesforceFieldMapService,
private readonly cache: CacheService,
private readonly config: ConfigService,
@Inject(Logger) private readonly logger: Logger
@ -34,15 +32,10 @@ export class ProvisioningProcessor extends WorkerHost {
});
// Guard: Only process if Salesforce Order is currently 'Activating'
const fields = this.fieldMapService.getFieldMap();
const order = await this.salesforceService.getOrder(sfOrderId);
const status = order
? ((Reflect.get(order, fields.order.activationStatus) as string | undefined) ?? "")
: "";
const lastErrorCodeField = fields.order.lastErrorCode;
const lastErrorCode = lastErrorCodeField
? ((order ? (Reflect.get(order, lastErrorCodeField) as string | undefined) : undefined) ?? "")
: "";
const status = order?.Activation_Status__c ?? "";
const lastErrorCode = order?.Activation_Error_Code__c ?? "";
if (status !== "Activating") {
this.logger.log("Skipping provisioning job: Order not in Activating state", {
sfOrderId,

View File

@ -15,7 +15,6 @@ import { OrderFulfillmentErrorService } from "./order-fulfillment-error.service"
import { SimFulfillmentService } from "./sim-fulfillment.service";
import { DistributedTransactionService } from "@bff/core/database/services/distributed-transaction.service";
import { getErrorMessage } from "@bff/core/utils/error.util";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import { type OrderSummary, type OrderDetails, type SalesforceOrderRecord, type SalesforceOrderItemRecord } from "@customer-portal/domain/orders";
import type { FulfillmentOrderDetails, FulfillmentOrderItem } from "../types/fulfillment.types";
@ -45,7 +44,6 @@ export interface OrderFulfillmentContext {
export class OrderFulfillmentOrchestrator {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly fieldMapService: SalesforceFieldMapService,
private readonly salesforceService: SalesforceService,
private readonly whmcsOrderService: WhmcsOrderService,
private readonly orderOrchestrator: OrderOrchestrator,
@ -135,17 +133,17 @@ export class OrderFulfillmentOrchestrator {
id: "sf_status_update",
description: "Update Salesforce order status to Activating",
execute: async () => {
const fields = this.fieldMapService.getFieldMap();
return await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Activating",
Activation_Status__c: "Activating",
});
},
rollback: async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Failed",
Activation_Status__c: "Failed",
});
},
critical: true,
@ -270,20 +268,20 @@ export class OrderFulfillmentOrchestrator {
id: "sf_success_update",
description: "Update Salesforce with success",
execute: async () => {
const fields = this.fieldMapService.getFieldMap();
return await this.salesforceService.updateOrder({
Id: sfOrderId,
Status: "Completed",
[fields.order.activationStatus]: "Activated",
[fields.order.whmcsOrderId]: whmcsAcceptResult?.orderId?.toString(),
Activation_Status__c: "Activated",
WHMCS_Order_ID__c: whmcsAcceptResult?.orderId?.toString(),
});
},
rollback: async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Failed",
Activation_Status__c: "Failed",
});
},
critical: true,
@ -370,10 +368,10 @@ export class OrderFulfillmentOrchestrator {
// Step 2: Update Salesforce status to "Activating"
await this.executeStep(context, "sf_status_update", async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Activating",
Activation_Status__c: "Activating",
});
});
@ -472,12 +470,12 @@ export class OrderFulfillmentOrchestrator {
// Step 8: Update Salesforce with success
await this.executeStep(context, "sf_success_update", async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
Status: "Completed",
[fields.order.activationStatus]: "Activated",
[fields.order.whmcsOrderId]: context.whmcsResult!.orderId.toString(),
Activation_Status__c: "Activated",
WHMCS_Order_ID__c: context.whmcsResult!.orderId.toString(),
});
});
@ -667,7 +665,7 @@ export class OrderFulfillmentOrchestrator {
): Promise<void> {
const errorCode = this.orderFulfillmentErrorService.determineErrorCode(error);
const userMessage = error.message;
const fields = this.fieldMapService.getFieldMap();
this.logger.error("Fulfillment orchestration failed", {
sfOrderId: context.sfOrderId,
@ -683,14 +681,14 @@ export class OrderFulfillmentOrchestrator {
Id: context.sfOrderId,
// Set overall Order.Status to Pending Review for manual attention
Status: "Pending Review",
[fields.order.activationStatus]: "Failed",
Activation_Status__c: "Failed",
Activation_Error_Code__c: (
this.orderFulfillmentErrorService.getShortCode(error) || String(errorCode)
)
.toString()
.substring(0, 60),
Activation_Error_Message__c: userMessage?.substring(0, 255),
};
updates[fields.order.lastErrorCode as string] = (
this.orderFulfillmentErrorService.getShortCode(error) || String(errorCode)
)
.toString()
.substring(0, 60);
updates[fields.order.lastErrorMessage as string] = userMessage?.substring(0, 255);
await this.salesforceService.updateOrder(updates as { Id: string; [key: string]: unknown });

View File

@ -5,7 +5,6 @@ import { WhmcsPaymentService } from "@bff/integrations/whmcs/services/whmcs-paym
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
import { getErrorMessage } from "@bff/core/utils/error.util";
import type { SalesforceOrderRecord } from "@customer-portal/domain/orders";
import { SalesforceFieldMapService, type SalesforceFieldMap } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import { sfOrderIdParamSchema } from "@customer-portal/domain/orders";
type OrderStringFieldKey = "activationStatus";
@ -24,7 +23,6 @@ export interface OrderFulfillmentValidationResult {
export class OrderFulfillmentValidator {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly fieldMapService: SalesforceFieldMapService,
private readonly salesforceService: SalesforceService,
private readonly whmcsPaymentService: WhmcsPaymentService,
private readonly mappingsService: MappingsService
@ -50,9 +48,8 @@ export class OrderFulfillmentValidator {
const sfOrder = await this.validateSalesforceOrder(sfOrderId);
// 2. Check if already provisioned (idempotency)
const fieldMap = this.fieldMapService.getFieldMap();
const rawWhmcs = Reflect.get(sfOrder, fieldMap.order.whmcsOrderId) as unknown;
const existingWhmcsOrderId = typeof rawWhmcs === "string" ? rawWhmcs : undefined;
const existingWhmcsOrderId = sfOrder.WHMCS_Order_ID__c;
if (existingWhmcsOrderId) {
this.logger.log("Order already provisioned", {
sfOrderId,
@ -116,11 +113,11 @@ export class OrderFulfillmentValidator {
throw new BadRequestException(`Cannot provision cancelled order ${sfOrderId}`);
}
const fieldMap = this.fieldMapService.getFieldMap();
this.logger.log("Salesforce order validated", {
sfOrderId,
status: order.Status,
activationStatus: pickOrderString(order, "activationStatus", fieldMap),
activationStatus: order.Activation_Status__c,
accountId: order.AccountId,
});
@ -159,16 +156,3 @@ export class OrderFulfillmentValidator {
}
}
}
function pickOrderString(
order: SalesforceOrderRecord,
key: OrderStringFieldKey,
fieldMap: SalesforceFieldMap
): string | undefined {
const field = fieldMap.order[key];
if (typeof field !== "string") {
return undefined;
}
const raw = Reflect.get(order, field) as unknown;
return typeof raw === "string" ? raw : undefined;
}

View File

@ -109,18 +109,15 @@ export class OrderOrchestrator {
const safeOrderId = assertSalesforceId(orderId, "orderId");
this.logger.log({ orderId: safeOrderId }, "Fetching order details with items");
const fieldMap = this.fieldMapService.getFieldMap();
const orderQueryFields = this.fieldMapService.getOrderQueryFields();
const orderQueryFields = buildOrderSelectFields(["Account.Name"]).join(", ");
const orderItemProduct2Fields = buildOrderItemProduct2Fields().map(f => `PricebookEntry.Product2.${f}`);
const orderItemSelect = [
this.fieldMapService.getOrderItemQueryFields(),
this.fieldMapService.getOrderItemProduct2Select(),
]
.filter(Boolean)
.join(", ");
...buildOrderItemSelectFields(),
...orderItemProduct2Fields,
].join(", ");
const orderSoql = `
SELECT ${orderQueryFields}, OrderNumber, TotalAmount,
Account.Name, CreatedDate, LastModifiedDate
SELECT ${orderQueryFields}
FROM Order
WHERE Id = '${safeOrderId}'
LIMIT 1
@ -155,8 +152,7 @@ export class OrderOrchestrator {
return OrderProviders.Salesforce.transformSalesforceOrderDetails(
order,
orderItems,
fieldMap
orderItems
);
} catch (error: unknown) {
this.logger.error("Failed to fetch order with items", {
@ -183,17 +179,15 @@ export class OrderOrchestrator {
return [];
}
const fieldMap = this.fieldMapService.getFieldMap();
const orderQueryFields = this.fieldMapService.getOrderQueryFields();
const orderQueryFields = buildOrderSelectFields().join(", ");
const orderItemProduct2Fields = buildOrderItemProduct2Fields().map(f => `PricebookEntry.Product2.${f}`);
const orderItemSelect = [
this.fieldMapService.getOrderItemQueryFields(),
this.fieldMapService.getOrderItemProduct2Select(),
]
.filter(Boolean)
.join(", ");
...buildOrderItemSelectFields(),
...orderItemProduct2Fields,
].join(", ");
const ordersSoql = `
SELECT ${orderQueryFields}, OrderNumber, TotalAmount, CreatedDate, LastModifiedDate
SELECT ${orderQueryFields}
FROM Order
WHERE AccountId = '${sfAccountId}'
ORDER BY CreatedDate DESC
@ -219,42 +213,52 @@ export class OrderOrchestrator {
return [];
}
const orderIdsClause = buildInClause(rawOrderIds, "orderIds");
const orderIds = rawOrderIds.map(id => assertSalesforceId(id, "orderId"));
const inClause = buildInClause(orderIds, "orderIds");
const itemsSoql = `
SELECT ${orderItemSelect}
FROM OrderItem
WHERE OrderId IN (${orderIdsClause})
ORDER BY OrderId, CreatedDate ASC
WHERE OrderId IN ${inClause}
ORDER BY CreatedDate ASC
`;
const itemsResult = (await this.sf.query(
itemsSoql
)) as SalesforceQueryResult<SalesforceOrderItemRecord>;
const allItems = itemsResult.records ?? [];
const allItems = itemsResult.records || [];
const itemsByOrder = allItems.reduce<Record<string, SalesforceOrderItemRecord[]>>(
(acc, record) => {
const orderId = typeof record.OrderId === "string" ? record.OrderId : undefined;
if (!orderId) return acc;
if (!acc[orderId]) acc[orderId] = [];
acc[orderId].push(record);
return acc;
// Group items by order ID
const itemsByOrder: Record<string, SalesforceOrderItemRecord[]> = {};
for (const item of allItems) {
const orderId = item.OrderId;
if (typeof orderId === "string") {
if (!itemsByOrder[orderId]) {
itemsByOrder[orderId] = [];
}
itemsByOrder[orderId].push(item);
}
}
this.logger.log(
{
userId,
orderCount: orders.length,
totalItems: allItems.length,
},
{}
"User orders retrieved with item summaries"
);
// Transform orders to domain types and return summary
return orders
.filter((order): order is SalesforceOrderRecord & { Id: string } => typeof order.Id === "string")
.map(order =>
OrderProviders.Salesforce.transformSalesforceOrderSummary(
order,
itemsByOrder[order.Id] ?? [],
fieldMap
itemsByOrder[order.Id] ?? []
)
);
} catch (error: unknown) {
this.logger.error("Failed to fetch user orders with items", {
this.logger.error("Failed to fetch orders for user", {
error: getErrorMessage(error),
userId,
});

View File

@ -2,8 +2,6 @@ import { Inject, Injectable, NotFoundException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
import { getStringField } from "@bff/modules/catalog/utils/salesforce-product.mapper";
import { z } from "zod";
import type {
SalesforceProduct2Record,
@ -33,7 +31,6 @@ export class OrderPricebookService {
constructor(
private readonly sf: SalesforceConnection,
private readonly fieldMapService: SalesforceFieldMapService,
private readonly configService: ConfigService,
@Inject(Logger) private readonly logger: Logger
) {}
@ -81,7 +78,7 @@ export class OrderPricebookService {
return new Map();
}
const fields = this.fieldMapService.getFieldMap();
const meta = new Map<string, PricebookProductMeta>();
for (let i = 0; i < uniqueSkus.length; i += this.chunkSize) {
@ -89,13 +86,13 @@ export class OrderPricebookService {
const whereIn = buildInClause(slice, "skus");
const soql =
`SELECT Id, Product2Id, UnitPrice, ` +
`Product2.${fields.product.sku}, ` +
`Product2.${fields.product.itemClass}, ` +
`Product2.${fields.product.internetOfferingType}, ` +
`Product2.${fields.product.internetPlanTier}, ` +
`Product2.${fields.product.vpnRegion} ` +
`Product2.StockKeepingUnit, ` +
`Product2.Item_Class__c, ` +
`Product2.Internet_Offering_Type__c, ` +
`Product2.Internet_Plan_Tier__c, ` +
`Product2.VPN_Region__c ` +
`FROM PricebookEntry ` +
`WHERE Pricebook2Id='${safePricebookId}' AND IsActive=true AND Product2.${fields.product.sku} IN (${whereIn})`;
`WHERE Pricebook2Id='${safePricebookId}' AND IsActive=true AND Product2.StockKeepingUnit IN ${whereIn}`;
try {
const res = (await this.sf.query(soql)) as SalesforceQueryResult<
@ -104,7 +101,7 @@ export class OrderPricebookService {
for (const record of res.records ?? []) {
const product = record.Product2 ?? undefined;
const sku = product ? getStringField(product, "sku", fields) : undefined;
const sku = product?.StockKeepingUnit ?? undefined;
if (!sku) continue;
const normalizedSku = sku.trim().toUpperCase();
@ -113,14 +110,10 @@ export class OrderPricebookService {
pricebookEntryId: assertSalesforceId(record.Id ?? undefined, "pricebookEntryId"),
product2Id: record.Product2Id ?? undefined,
unitPrice: typeof record.UnitPrice === "number" ? record.UnitPrice : undefined,
itemClass: product ? getStringField(product, "itemClass", fields) : undefined,
internetOfferingType: product
? getStringField(product, "internetOfferingType", fields)
: undefined,
internetPlanTier: product
? getStringField(product, "internetPlanTier", fields)
: undefined,
vpnRegion: product ? getStringField(product, "vpnRegion", fields) : undefined,
itemClass: product?.Item_Class__c ?? undefined,
internetOfferingType: product?.Internet_Offering_Type__c ?? undefined,
internetPlanTier: product?.Internet_Plan_Tier__c ?? undefined,
vpnRegion: product?.VPN_Region__c ?? undefined,
});
}
} catch (error) {

View File

@ -94,88 +94,6 @@ export type OrderType = string;
*/
export type UserMapping = Pick<UserIdMapping, "userId" | "whmcsClientId" | "sfAccountId">;
// ============================================================================
// Salesforce Field Configuration (Provider-Specific, Not Validated)
// ============================================================================
/**
* Configuration for MNP (Mobile Number Portability) field mappings.
* Maps logical field names to actual Salesforce custom field names.
*/
export interface SalesforceOrderMnpFieldConfig {
application: string;
reservationNumber: string;
expiryDate: string;
phoneNumber: string;
mvnoAccountNumber: string;
portingDateOfBirth: string;
portingFirstName: string;
portingLastName: string;
portingFirstNameKatakana: string;
portingLastNameKatakana: string;
portingGender: string;
}
/**
* Configuration for billing address field mappings.
* Maps logical field names to actual Salesforce custom field names.
*/
export interface SalesforceOrderBillingFieldConfig {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
}
/**
* Configuration for Order field mappings.
* Maps logical field names to actual Salesforce custom field names.
*/
export interface SalesforceOrderFieldConfig {
orderType: string;
activationType: string;
activationScheduledAt: string;
activationStatus: string;
internetPlanTier: string;
installationType: string;
weekendInstall: string;
accessMode: string;
hikariDenwa: string;
vpnRegion: string;
simType: string;
eid: string;
simVoiceMail: string;
simCallWaiting: string;
mnp: SalesforceOrderMnpFieldConfig;
whmcsOrderId: string;
lastErrorCode?: string;
lastErrorMessage?: string;
lastAttemptAt?: string;
addressChanged: string;
billing: SalesforceOrderBillingFieldConfig;
}
/**
* Configuration for OrderItem field mappings.
* Maps logical field names to actual Salesforce custom field names.
*/
export interface SalesforceOrderItemFieldConfig {
billingCycle: string;
whmcsServiceId: string;
}
/**
* Complete Salesforce field configuration for orders.
* Aggregates all field configurations needed for order operations.
*/
export interface SalesforceOrdersFieldConfig {
account: SalesforceAccountFieldMap;
product: SalesforceProductFieldMap;
order: SalesforceOrderFieldConfig;
orderItem: SalesforceOrderItemFieldConfig;
}
// ============================================================================
// Re-export Types from Schema (Schema-First Approach)
// ============================================================================

View File

@ -9,15 +9,6 @@
// Business types
export { type OrderCreationType, type OrderStatus, type OrderType, type UserMapping } from "./contract";
// Provider-specific types
export type {
SalesforceOrderMnpFieldConfig,
SalesforceOrderBillingFieldConfig,
SalesforceOrderFieldConfig,
SalesforceOrderItemFieldConfig,
SalesforceOrdersFieldConfig,
} from "./contract";
// Schemas (includes derived types)
export * from "./schema";