diff --git a/apps/bff/src/integrations/salesforce/salesforce.module.ts b/apps/bff/src/integrations/salesforce/salesforce.module.ts index ffd47d14..33f3f4e6 100644 --- a/apps/bff/src/integrations/salesforce/salesforce.module.ts +++ b/apps/bff/src/integrations/salesforce/salesforce.module.ts @@ -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 {} diff --git a/apps/bff/src/integrations/salesforce/salesforce.service.ts b/apps/bff/src/integrations/salesforce/salesforce.service.ts index aa896825..fcae25d0 100644 --- a/apps/bff/src/integrations/salesforce/salesforce.service.ts +++ b/apps/bff/src/integrations/salesforce/salesforce.service.ts @@ -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}' diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-field-config.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-field-config.service.ts deleted file mode 100644 index fb53ce8b..00000000 --- a/apps/bff/src/integrations/salesforce/services/salesforce-field-config.service.ts +++ /dev/null @@ -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("PRODUCT_SKU_FIELD")!, - portalCategory: this.configService.get("PRODUCT_PORTAL_CATEGORY_FIELD")!, - portalCatalog: this.configService.get("PRODUCT_PORTAL_CATALOG_FIELD")!, - portalAccessible: this.configService.get("PRODUCT_PORTAL_ACCESSIBLE_FIELD")!, - itemClass: this.configService.get("PRODUCT_ITEM_CLASS_FIELD")!, - billingCycle: this.configService.get("PRODUCT_BILLING_CYCLE_FIELD")!, - whmcsProductId: this.configService.get("PRODUCT_WHMCS_PRODUCT_ID_FIELD")!, - whmcsProductName: this.configService.get("PRODUCT_WHMCS_PRODUCT_NAME_FIELD")!, - internetPlanTier: this.configService.get("PRODUCT_INTERNET_PLAN_TIER_FIELD")!, - internetOfferingType: this.configService.get("PRODUCT_INTERNET_OFFERING_TYPE_FIELD")!, - displayOrder: this.configService.get("PRODUCT_DISPLAY_ORDER_FIELD")!, - bundledAddon: this.configService.get("PRODUCT_BUNDLED_ADDON_FIELD")!, - isBundledAddon: this.configService.get("PRODUCT_IS_BUNDLED_ADDON_FIELD")!, - simDataSize: this.configService.get("PRODUCT_SIM_DATA_SIZE_FIELD")!, - simPlanType: this.configService.get("PRODUCT_SIM_PLAN_TYPE_FIELD")!, - simHasFamilyDiscount: this.configService.get("PRODUCT_SIM_HAS_FAMILY_DISCOUNT_FIELD")!, - vpnRegion: this.configService.get("PRODUCT_VPN_REGION_FIELD")!, - }; - - const fieldConfig: SalesforceFieldConfig = { - account: { - internetEligibility: this.configService.get("ACCOUNT_INTERNET_ELIGIBILITY_FIELD")!, - customerNumber: this.configService.get("ACCOUNT_CUSTOMER_NUMBER_FIELD")!, - }, - product, - order: { - orderType: this.configService.get("ORDER_TYPE_FIELD")!, - activationType: this.configService.get("ORDER_ACTIVATION_TYPE_FIELD")!, - activationScheduledAt: this.configService.get( - "ORDER_ACTIVATION_SCHEDULED_AT_FIELD" - )!, - activationStatus: this.configService.get("ORDER_ACTIVATION_STATUS_FIELD")!, - internetPlanTier: this.configService.get("ORDER_INTERNET_PLAN_TIER_FIELD")!, - installationType: this.configService.get("ORDER_INSTALLATION_TYPE_FIELD")!, - weekendInstall: this.configService.get("ORDER_WEEKEND_INSTALL_FIELD")!, - accessMode: this.configService.get("ORDER_ACCESS_MODE_FIELD")!, - hikariDenwa: this.configService.get("ORDER_HIKARI_DENWA_FIELD")!, - vpnRegion: this.configService.get("ORDER_VPN_REGION_FIELD")!, - simType: this.configService.get("ORDER_SIM_TYPE_FIELD")!, - eid: this.configService.get("ORDER_EID_FIELD")!, - simVoiceMail: this.configService.get("ORDER_SIM_VOICE_MAIL_FIELD")!, - simCallWaiting: this.configService.get("ORDER_SIM_CALL_WAITING_FIELD")!, - mnp: { - application: this.configService.get("ORDER_MNP_APPLICATION_FIELD")!, - reservationNumber: this.configService.get("ORDER_MNP_RESERVATION_FIELD")!, - expiryDate: this.configService.get("ORDER_MNP_EXPIRY_FIELD")!, - phoneNumber: this.configService.get("ORDER_MNP_PHONE_FIELD")!, - mvnoAccountNumber: this.configService.get("ORDER_MVNO_ACCOUNT_NUMBER_FIELD")!, - portingDateOfBirth: this.configService.get("ORDER_PORTING_DOB_FIELD")!, - portingFirstName: this.configService.get("ORDER_PORTING_FIRST_NAME_FIELD")!, - portingLastName: this.configService.get("ORDER_PORTING_LAST_NAME_FIELD")!, - portingFirstNameKatakana: this.configService.get( - "ORDER_PORTING_FIRST_NAME_KATAKANA_FIELD" - )!, - portingLastNameKatakana: this.configService.get( - "ORDER_PORTING_LAST_NAME_KATAKANA_FIELD" - )!, - portingGender: this.configService.get("ORDER_PORTING_GENDER_FIELD")!, - }, - whmcsOrderId: this.configService.get("ORDER_WHMCS_ORDER_ID_FIELD")!, - lastErrorCode: this.configService.get("ORDER_ACTIVATION_ERROR_CODE_FIELD"), - lastErrorMessage: this.configService.get("ORDER_ACTIVATION_ERROR_MESSAGE_FIELD"), - lastAttemptAt: this.configService.get("ORDER_ACTIVATION_LAST_ATTEMPT_AT_FIELD"), - addressChanged: this.configService.get("ORDER_ADDRESS_CHANGED_FIELD")!, - billing: { - street: this.configService.get("ORDER_BILLING_STREET_FIELD")!, - city: this.configService.get("ORDER_BILLING_CITY_FIELD")!, - state: this.configService.get("ORDER_BILLING_STATE_FIELD")!, - postalCode: this.configService.get("ORDER_BILLING_POSTAL_CODE_FIELD")!, - country: this.configService.get("ORDER_BILLING_COUNTRY_FIELD")!, - }, - }, - orderItem: { - billingCycle: this.configService.get("ORDER_ITEM_BILLING_CYCLE_FIELD")!, - whmcsServiceId: this.configService.get("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(", "); - } -} diff --git a/apps/bff/src/modules/catalog/services/base-catalog.service.ts b/apps/bff/src/modules/catalog/services/base-catalog.service.ts index 05123ab4..8f6d277c 100644 --- a/apps/bff/src/modules/catalog/services/base-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/base-catalog.service.ts @@ -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( 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` ); } } diff --git a/apps/bff/src/modules/catalog/services/internet-catalog.service.ts b/apps/bff/src/modules/catalog/services/internet-catalog.service.ts index ab3d5285..2c71bd84 100644 --- a/apps/bff/src/modules/catalog/services/internet-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/internet-catalog.service.ts @@ -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 { - 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( 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 { - 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( 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 { - 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( 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) { diff --git a/apps/bff/src/modules/catalog/services/sim-catalog.service.ts b/apps/bff/src/modules/catalog/services/sim-catalog.service.ts index 91ff8a4d..92384d40 100644 --- a/apps/bff/src/modules/catalog/services/sim-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/sim-catalog.service.ts @@ -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 { - 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( 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 { - const fields = this.getFields(); const soql = this.buildProductQuery("SIM", "Activation", []); const records = await this.executeQuery( 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 { - 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( 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, diff --git a/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts b/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts index 4a557e33..4dd77cbf 100644 --- a/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts +++ b/apps/bff/src/modules/catalog/services/vpn-catalog.service.ts @@ -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 { - 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( 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 { - 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( 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, diff --git a/apps/bff/src/modules/catalog/utils/salesforce-product.mapper.ts b/apps/bff/src/modules/catalog/utils/salesforce-product.mapper.ts deleted file mode 100644 index 4c8fd741..00000000 --- a/apps/bff/src/modules/catalog/utils/salesforce-product.mapper.ts +++ /dev/null @@ -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( - 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 { - 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, - }; -} diff --git a/apps/bff/src/modules/orders/queue/provisioning.processor.ts b/apps/bff/src/modules/orders/queue/provisioning.processor.ts index a0d84440..d1353e0b 100644 --- a/apps/bff/src/modules/orders/queue/provisioning.processor.ts +++ b/apps/bff/src/modules/orders/queue/provisioning.processor.ts @@ -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, diff --git a/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts index a9135cf6..7565efe7 100644 --- a/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts +++ b/apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts @@ -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 { 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 }); diff --git a/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts b/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts index ecac695c..63cf7098 100644 --- a/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts +++ b/apps/bff/src/modules/orders/services/order-fulfillment-validator.service.ts @@ -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; -} diff --git a/apps/bff/src/modules/orders/services/order-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-orchestrator.service.ts index e3b30561..b738918a 100644 --- a/apps/bff/src/modules/orders/services/order-orchestrator.service.ts +++ b/apps/bff/src/modules/orders/services/order-orchestrator.service.ts @@ -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; - const allItems = itemsResult.records ?? []; + const allItems = itemsResult.records || []; - const itemsByOrder = allItems.reduce>( - (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 = {}; + 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, }); diff --git a/apps/bff/src/modules/orders/services/order-pricebook.service.ts b/apps/bff/src/modules/orders/services/order-pricebook.service.ts index e54400ce..9db4b718 100644 --- a/apps/bff/src/modules/orders/services/order-pricebook.service.ts +++ b/apps/bff/src/modules/orders/services/order-pricebook.service.ts @@ -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(); 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) { diff --git a/packages/domain/orders/contract.ts b/packages/domain/orders/contract.ts index d0036ce5..f3cbc748 100644 --- a/packages/domain/orders/contract.ts +++ b/packages/domain/orders/contract.ts @@ -94,88 +94,6 @@ export type OrderType = string; */ export type UserMapping = Pick; -// ============================================================================ -// 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) // ============================================================================ diff --git a/packages/domain/orders/index.ts b/packages/domain/orders/index.ts index 532d4caf..a295412b 100644 --- a/packages/domain/orders/index.ts +++ b/packages/domain/orders/index.ts @@ -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";