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:
parent
88d58f9ac5
commit
e5c5f352f2
@ -4,7 +4,6 @@ import { QueueModule } from "@bff/core/queue/queue.module";
|
|||||||
import { SalesforceService } from "./salesforce.service";
|
import { SalesforceService } from "./salesforce.service";
|
||||||
import { SalesforceConnection } from "./services/salesforce-connection.service";
|
import { SalesforceConnection } from "./services/salesforce-connection.service";
|
||||||
import { SalesforceAccountService } from "./services/salesforce-account.service";
|
import { SalesforceAccountService } from "./services/salesforce-account.service";
|
||||||
import { SalesforceFieldMapService } from "./services/salesforce-field-config.service";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [QueueModule, ConfigModule],
|
imports: [QueueModule, ConfigModule],
|
||||||
@ -12,8 +11,7 @@ import { SalesforceFieldMapService } from "./services/salesforce-field-config.se
|
|||||||
SalesforceConnection,
|
SalesforceConnection,
|
||||||
SalesforceAccountService,
|
SalesforceAccountService,
|
||||||
SalesforceService,
|
SalesforceService,
|
||||||
SalesforceFieldMapService,
|
|
||||||
],
|
],
|
||||||
exports: [SalesforceService, SalesforceConnection, SalesforceFieldMapService],
|
exports: [SalesforceService, SalesforceConnection],
|
||||||
})
|
})
|
||||||
export class SalesforceModule {}
|
export class SalesforceModule {}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { Logger } from "nestjs-pino";
|
|||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||||
import { SalesforceConnection } from "./services/salesforce-connection.service";
|
import { SalesforceConnection } from "./services/salesforce-connection.service";
|
||||||
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
|
|
||||||
import {
|
import {
|
||||||
SalesforceAccountService,
|
SalesforceAccountService,
|
||||||
type AccountData,
|
type AccountData,
|
||||||
@ -27,7 +26,6 @@ export class SalesforceService implements OnModuleInit {
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private connection: SalesforceConnection,
|
private connection: SalesforceConnection,
|
||||||
private accountService: SalesforceAccountService,
|
private accountService: SalesforceAccountService,
|
||||||
private fieldMapService: SalesforceFieldMapService,
|
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -118,10 +116,9 @@ export class SalesforceService implements OnModuleInit {
|
|||||||
throw new Error("Salesforce connection not available");
|
throw new Error("Salesforce connection not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
const result = (await this.connection.query(
|
const result = (await this.connection.query(
|
||||||
`SELECT Id, Status, ${fields.order.activationStatus}, ${fields.order.whmcsOrderId},
|
`SELECT Id, Status, Type, Activation_Status__c, WHMCS_Order_ID__c,
|
||||||
${fields.order.lastErrorCode}, ${fields.order.lastErrorMessage},
|
Activation_Error_Code__c, Activation_Error_Message__c,
|
||||||
AccountId, Account.Name
|
AccountId, Account.Name
|
||||||
FROM Order
|
FROM Order
|
||||||
WHERE Id = '${orderId}'
|
WHERE Id = '${orderId}'
|
||||||
|
|||||||
@ -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(", ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,7 +2,6 @@ import { Injectable, Inject } from "@nestjs/common";
|
|||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
||||||
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
|
|
||||||
import {
|
import {
|
||||||
assertSalesforceId,
|
assertSalesforceId,
|
||||||
sanitizeSoqlLiteral,
|
sanitizeSoqlLiteral,
|
||||||
@ -12,6 +11,7 @@ import type {
|
|||||||
SalesforceProduct2WithPricebookEntries,
|
SalesforceProduct2WithPricebookEntries,
|
||||||
SalesforcePricebookEntryRecord,
|
SalesforcePricebookEntryRecord,
|
||||||
} from "@customer-portal/domain/catalog";
|
} from "@customer-portal/domain/catalog";
|
||||||
|
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
|
||||||
import type { SalesforceQueryResult } from "@customer-portal/domain/orders";
|
import type { SalesforceQueryResult } from "@customer-portal/domain/orders";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -20,7 +20,6 @@ export class BaseCatalogService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly sf: SalesforceConnection,
|
protected readonly sf: SalesforceConnection,
|
||||||
protected readonly fieldMapService: SalesforceFieldMapService,
|
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
@Inject(Logger) protected readonly logger: Logger
|
@Inject(Logger) protected readonly logger: Logger
|
||||||
) {
|
) {
|
||||||
@ -28,10 +27,6 @@ export class BaseCatalogService {
|
|||||||
this.portalPriceBookId = assertSalesforceId(portalPricebook, "PORTAL_PRICEBOOK_ID");
|
this.portalPriceBookId = assertSalesforceId(portalPricebook, "PORTAL_PRICEBOOK_ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getFields() {
|
|
||||||
return this.fieldMapService.getFieldMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeQuery<TRecord extends SalesforceProduct2WithPricebookEntries>(
|
protected async executeQuery<TRecord extends SalesforceProduct2WithPricebookEntries>(
|
||||||
soql: string,
|
soql: string,
|
||||||
context: string
|
context: string
|
||||||
@ -52,13 +47,9 @@ export class BaseCatalogService {
|
|||||||
protected extractPricebookEntry(
|
protected extractPricebookEntry(
|
||||||
record: SalesforceProduct2WithPricebookEntries
|
record: SalesforceProduct2WithPricebookEntries
|
||||||
): SalesforcePricebookEntryRecord | undefined {
|
): SalesforcePricebookEntryRecord | undefined {
|
||||||
const pricebookEntries = record.PricebookEntries?.records;
|
const entry = CatalogProviders.Salesforce.extractPricebookEntry(record);
|
||||||
const entry = Array.isArray(pricebookEntries) ? pricebookEntries[0] : undefined;
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
const fields = this.getFields();
|
const sku = record.StockKeepingUnit ?? undefined;
|
||||||
const skuField = fields.product.sku;
|
|
||||||
const skuRaw = Reflect.get(record, skuField) as unknown;
|
|
||||||
const sku = typeof skuRaw === "string" ? skuRaw : undefined;
|
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`No pricebook entry found for product ${String(record.Name)} (SKU: ${String(sku ?? "")}). Pricebook ID: ${this.portalPriceBookId}.`
|
`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[] = [],
|
additionalFields: string[] = [],
|
||||||
additionalConditions: string = ""
|
additionalConditions: string = ""
|
||||||
): string {
|
): string {
|
||||||
const fields = this.getFields();
|
|
||||||
const baseFields = [
|
const baseFields = [
|
||||||
"Id",
|
"Id",
|
||||||
"Name",
|
"Name",
|
||||||
fields.product.sku,
|
"StockKeepingUnit",
|
||||||
fields.product.portalCategory,
|
"Portal_Category__c",
|
||||||
fields.product.itemClass,
|
"Item_Class__c",
|
||||||
];
|
];
|
||||||
const allFields = [...baseFields, ...additionalFields].join(", ");
|
const allFields = [...baseFields, ...additionalFields].join(", ");
|
||||||
|
|
||||||
@ -91,11 +81,11 @@ export class BaseCatalogService {
|
|||||||
SELECT ${allFields},
|
SELECT ${allFields},
|
||||||
(SELECT Id, UnitPrice, Pricebook2Id, Product2Id, IsActive FROM PricebookEntries WHERE Pricebook2Id = '${this.portalPriceBookId}' AND IsActive = true LIMIT 1)
|
(SELECT Id, UnitPrice, Pricebook2Id, Product2Id, IsActive FROM PricebookEntries WHERE Pricebook2Id = '${this.portalPriceBookId}' AND IsActive = true LIMIT 1)
|
||||||
FROM Product2
|
FROM Product2
|
||||||
WHERE ${fields.product.portalCategory} = '${safeCategory}'
|
WHERE Portal_Category__c = '${safeCategory}'
|
||||||
AND ${fields.product.itemClass} = '${safeItemClass}'
|
AND Item_Class__c = '${safeItemClass}'
|
||||||
AND ${fields.product.portalAccessible} = true
|
AND Portal_Accessible__c = true
|
||||||
${additionalConditions}
|
${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 {
|
protected buildCatalogServiceQuery(category: string, additionalFields: string[] = []): string {
|
||||||
const fields = this.getFields();
|
|
||||||
return this.buildProductQuery(
|
return this.buildProductQuery(
|
||||||
category,
|
category,
|
||||||
"Service",
|
"Service",
|
||||||
additionalFields,
|
additionalFields,
|
||||||
`AND ${fields.product.portalCatalog} = true`
|
`AND Portal_Catalog__c = true`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,17 +7,12 @@ import type {
|
|||||||
InternetInstallationCatalogItem,
|
InternetInstallationCatalogItem,
|
||||||
InternetAddonCatalogItem,
|
InternetAddonCatalogItem,
|
||||||
} from "@customer-portal/domain/catalog";
|
} from "@customer-portal/domain/catalog";
|
||||||
|
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
|
||||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.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 { Logger } from "nestjs-pino";
|
||||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||||
import { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util";
|
import { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util";
|
||||||
import {
|
|
||||||
mapInternetPlan,
|
|
||||||
mapInternetInstallation,
|
|
||||||
mapInternetAddon,
|
|
||||||
} from "@bff/modules/catalog/utils/salesforce-product.mapper";
|
|
||||||
|
|
||||||
interface SalesforceAccount {
|
interface SalesforceAccount {
|
||||||
Id: string;
|
Id: string;
|
||||||
@ -28,20 +23,18 @@ interface SalesforceAccount {
|
|||||||
export class InternetCatalogService extends BaseCatalogService {
|
export class InternetCatalogService extends BaseCatalogService {
|
||||||
constructor(
|
constructor(
|
||||||
sf: SalesforceConnection,
|
sf: SalesforceConnection,
|
||||||
fieldMapService: SalesforceFieldMapService,
|
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
@Inject(Logger) logger: Logger,
|
@Inject(Logger) logger: Logger,
|
||||||
private mappingsService: MappingsService
|
private mappingsService: MappingsService
|
||||||
) {
|
) {
|
||||||
super(sf, fieldMapService, configService, logger);
|
super(sf, configService, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlans(): Promise<InternetPlanCatalogItem[]> {
|
async getPlans(): Promise<InternetPlanCatalogItem[]> {
|
||||||
const fields = this.getFields();
|
|
||||||
const soql = this.buildCatalogServiceQuery("Internet", [
|
const soql = this.buildCatalogServiceQuery("Internet", [
|
||||||
fields.product.internetPlanTier,
|
"Internet_Plan_Tier__c",
|
||||||
fields.product.internetOfferingType,
|
"Internet_Offering_Type__c",
|
||||||
fields.product.displayOrder,
|
"Catalog_Order__c",
|
||||||
]);
|
]);
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
@ -50,15 +43,14 @@ export class InternetCatalogService extends BaseCatalogService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
return mapInternetPlan(record, fields, entry);
|
return CatalogProviders.Salesforce.mapInternetPlan(record, entry);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstallations(): Promise<InternetInstallationCatalogItem[]> {
|
async getInstallations(): Promise<InternetInstallationCatalogItem[]> {
|
||||||
const fields = this.getFields();
|
|
||||||
const soql = this.buildProductQuery("Internet", "Installation", [
|
const soql = this.buildProductQuery("Internet", "Installation", [
|
||||||
fields.product.billingCycle,
|
"Billing_Cycle__c",
|
||||||
fields.product.displayOrder,
|
"Catalog_Order__c",
|
||||||
]);
|
]);
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
@ -70,18 +62,17 @@ export class InternetCatalogService extends BaseCatalogService {
|
|||||||
return records
|
return records
|
||||||
.map(record => {
|
.map(record => {
|
||||||
const entry = this.extractPricebookEntry(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));
|
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAddons(): Promise<InternetAddonCatalogItem[]> {
|
async getAddons(): Promise<InternetAddonCatalogItem[]> {
|
||||||
const fields = this.getFields();
|
|
||||||
const soql = this.buildProductQuery("Internet", "Add-on", [
|
const soql = this.buildProductQuery("Internet", "Add-on", [
|
||||||
fields.product.billingCycle,
|
"Billing_Cycle__c",
|
||||||
fields.product.displayOrder,
|
"Catalog_Order__c",
|
||||||
fields.product.bundledAddon,
|
"Bundled_Addon__c",
|
||||||
fields.product.isBundledAddon,
|
"Is_Bundled_Addon__c",
|
||||||
]);
|
]);
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
@ -93,7 +84,7 @@ export class InternetCatalogService extends BaseCatalogService {
|
|||||||
return records
|
return records
|
||||||
.map(record => {
|
.map(record => {
|
||||||
const entry = this.extractPricebookEntry(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));
|
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
|
||||||
}
|
}
|
||||||
@ -120,9 +111,8 @@ export class InternetCatalogService extends BaseCatalogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get customer's eligibility from Salesforce
|
// Get customer's eligibility from Salesforce
|
||||||
const fields = this.getFields();
|
|
||||||
const sfAccountId = assertSalesforceId(mapping.sfAccountId, "sfAccountId");
|
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");
|
const accounts = await this.executeQuery(soql, "Customer Eligibility");
|
||||||
|
|
||||||
if (accounts.length === 0) {
|
if (accounts.length === 0) {
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
import { Injectable, Inject } from "@nestjs/common";
|
import { Injectable, Inject } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { BaseCatalogService } from "./base-catalog.service";
|
import { BaseCatalogService } from "./base-catalog.service";
|
||||||
import { SalesforceFieldMapService } from "@bff/integrations/salesforce/services/salesforce-field-config.service";
|
|
||||||
import type {
|
import type {
|
||||||
SalesforceProduct2WithPricebookEntries,
|
SalesforceProduct2WithPricebookEntries,
|
||||||
SimCatalogProduct,
|
SimCatalogProduct,
|
||||||
SimActivationFeeCatalogItem,
|
SimActivationFeeCatalogItem,
|
||||||
} from "@customer-portal/domain/catalog";
|
} from "@customer-portal/domain/catalog";
|
||||||
import {
|
import { Providers as CatalogProviders } from "@customer-portal/domain/catalog";
|
||||||
mapSimProduct,
|
|
||||||
mapSimActivationFee,
|
|
||||||
} from "@bff/modules/catalog/utils/salesforce-product.mapper";
|
|
||||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
@ -20,22 +16,20 @@ import { WhmcsConnectionOrchestratorService } from "@bff/integrations/whmcs/conn
|
|||||||
export class SimCatalogService extends BaseCatalogService {
|
export class SimCatalogService extends BaseCatalogService {
|
||||||
constructor(
|
constructor(
|
||||||
sf: SalesforceConnection,
|
sf: SalesforceConnection,
|
||||||
fieldMapService: SalesforceFieldMapService,
|
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
@Inject(Logger) logger: Logger,
|
@Inject(Logger) logger: Logger,
|
||||||
private mappingsService: MappingsService,
|
private mappingsService: MappingsService,
|
||||||
private whmcs: WhmcsConnectionOrchestratorService
|
private whmcs: WhmcsConnectionOrchestratorService
|
||||||
) {
|
) {
|
||||||
super(sf, fieldMapService, configService, logger);
|
super(sf, configService, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlans(): Promise<SimCatalogProduct[]> {
|
async getPlans(): Promise<SimCatalogProduct[]> {
|
||||||
const fields = this.getFields();
|
|
||||||
const soql = this.buildCatalogServiceQuery("SIM", [
|
const soql = this.buildCatalogServiceQuery("SIM", [
|
||||||
fields.product.simDataSize,
|
"SIM_Data_Size__c",
|
||||||
fields.product.simPlanType,
|
"SIM_Plan_Type__c",
|
||||||
fields.product.simHasFamilyDiscount,
|
"SIM_Has_Family_Discount__c",
|
||||||
fields.product.displayOrder,
|
"Catalog_Order__c",
|
||||||
]);
|
]);
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
@ -44,7 +38,7 @@ export class SimCatalogService extends BaseCatalogService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const product = mapSimProduct(record, fields, entry);
|
const product = CatalogProviders.Salesforce.mapSimProduct(record, entry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
@ -54,7 +48,6 @@ export class SimCatalogService extends BaseCatalogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getActivationFees(): Promise<SimActivationFeeCatalogItem[]> {
|
async getActivationFees(): Promise<SimActivationFeeCatalogItem[]> {
|
||||||
const fields = this.getFields();
|
|
||||||
const soql = this.buildProductQuery("SIM", "Activation", []);
|
const soql = this.buildProductQuery("SIM", "Activation", []);
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
@ -63,17 +56,16 @@ export class SimCatalogService extends BaseCatalogService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
return mapSimActivationFee(record, fields, entry);
|
return CatalogProviders.Salesforce.mapSimActivationFee(record, entry);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAddons(): Promise<SimCatalogProduct[]> {
|
async getAddons(): Promise<SimCatalogProduct[]> {
|
||||||
const fields = this.getFields();
|
|
||||||
const soql = this.buildProductQuery("SIM", "Add-on", [
|
const soql = this.buildProductQuery("SIM", "Add-on", [
|
||||||
fields.product.billingCycle,
|
"Billing_Cycle__c",
|
||||||
fields.product.displayOrder,
|
"Catalog_Order__c",
|
||||||
fields.product.bundledAddon,
|
"Bundled_Addon__c",
|
||||||
fields.product.isBundledAddon,
|
"Is_Bundled_Addon__c",
|
||||||
]);
|
]);
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
@ -83,7 +75,7 @@ export class SimCatalogService extends BaseCatalogService {
|
|||||||
return records
|
return records
|
||||||
.map(record => {
|
.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const product = mapSimProduct(record, fields, entry);
|
const product = CatalogProviders.Salesforce.mapSimProduct(record, entry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
|
|||||||
@ -2,26 +2,23 @@ import { Injectable, Inject } from "@nestjs/common";
|
|||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
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 { BaseCatalogService } from "./base-catalog.service";
|
||||||
import type { SalesforceProduct2WithPricebookEntries, VpnCatalogProduct } from "@customer-portal/domain/catalog";
|
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()
|
@Injectable()
|
||||||
export class VpnCatalogService extends BaseCatalogService {
|
export class VpnCatalogService extends BaseCatalogService {
|
||||||
constructor(
|
constructor(
|
||||||
sf: SalesforceConnection,
|
sf: SalesforceConnection,
|
||||||
fieldMapService: SalesforceFieldMapService,
|
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
@Inject(Logger) logger: Logger
|
@Inject(Logger) logger: Logger
|
||||||
) {
|
) {
|
||||||
super(sf, fieldMapService, configService, logger);
|
super(sf, configService, logger);
|
||||||
}
|
}
|
||||||
async getPlans(): Promise<VpnCatalogProduct[]> {
|
async getPlans(): Promise<VpnCatalogProduct[]> {
|
||||||
const fields = this.getFields();
|
|
||||||
const soql = this.buildCatalogServiceQuery("VPN", [
|
const soql = this.buildCatalogServiceQuery("VPN", [
|
||||||
fields.product.vpnRegion,
|
"VPN_Region__c",
|
||||||
fields.product.displayOrder,
|
"Catalog_Order__c",
|
||||||
]);
|
]);
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
@ -30,7 +27,7 @@ export class VpnCatalogService extends BaseCatalogService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const entry = this.extractPricebookEntry(record);
|
const entry = this.extractPricebookEntry(record);
|
||||||
const product = mapVpnProduct(record, fields, entry);
|
const product = CatalogProviders.Salesforce.mapVpnProduct(record, entry);
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
description: product.description || product.name,
|
description: product.description || product.name,
|
||||||
@ -39,8 +36,7 @@ export class VpnCatalogService extends BaseCatalogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getActivationFees(): Promise<VpnCatalogProduct[]> {
|
async getActivationFees(): Promise<VpnCatalogProduct[]> {
|
||||||
const fields = this.getFields();
|
const soql = this.buildProductQuery("VPN", "Activation", ["VPN_Region__c"]);
|
||||||
const soql = this.buildProductQuery("VPN", "Activation", [fields.product.vpnRegion]);
|
|
||||||
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
const records = await this.executeQuery<SalesforceProduct2WithPricebookEntries>(
|
||||||
soql,
|
soql,
|
||||||
"VPN Activation Fees"
|
"VPN Activation Fees"
|
||||||
@ -48,7 +44,7 @@ export class VpnCatalogService extends BaseCatalogService {
|
|||||||
|
|
||||||
return records.map(record => {
|
return records.map(record => {
|
||||||
const pricebookEntry = this.extractPricebookEntry(record);
|
const pricebookEntry = this.extractPricebookEntry(record);
|
||||||
const product = mapVpnProduct(record, fields, pricebookEntry);
|
const product = CatalogProviders.Salesforce.mapVpnProduct(record, pricebookEntry);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -3,7 +3,6 @@ import { Injectable, Inject } from "@nestjs/common";
|
|||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { OrderFulfillmentOrchestrator } from "../services/order-fulfillment-orchestrator.service";
|
import { OrderFulfillmentOrchestrator } from "../services/order-fulfillment-orchestrator.service";
|
||||||
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.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 type { ProvisioningJobData } from "./provisioning.queue";
|
||||||
import { CacheService } from "@bff/infra/cache/cache.service";
|
import { CacheService } from "@bff/infra/cache/cache.service";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
@ -16,7 +15,6 @@ export class ProvisioningProcessor extends WorkerHost {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly orchestrator: OrderFulfillmentOrchestrator,
|
private readonly orchestrator: OrderFulfillmentOrchestrator,
|
||||||
private readonly salesforceService: SalesforceService,
|
private readonly salesforceService: SalesforceService,
|
||||||
private readonly fieldMapService: SalesforceFieldMapService,
|
|
||||||
private readonly cache: CacheService,
|
private readonly cache: CacheService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
@ -34,15 +32,10 @@ export class ProvisioningProcessor extends WorkerHost {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Guard: Only process if Salesforce Order is currently 'Activating'
|
// Guard: Only process if Salesforce Order is currently 'Activating'
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
const order = await this.salesforceService.getOrder(sfOrderId);
|
const order = await this.salesforceService.getOrder(sfOrderId);
|
||||||
const status = order
|
const status = order?.Activation_Status__c ?? "";
|
||||||
? ((Reflect.get(order, fields.order.activationStatus) as string | undefined) ?? "")
|
const lastErrorCode = order?.Activation_Error_Code__c ?? "";
|
||||||
: "";
|
|
||||||
const lastErrorCodeField = fields.order.lastErrorCode;
|
|
||||||
const lastErrorCode = lastErrorCodeField
|
|
||||||
? ((order ? (Reflect.get(order, lastErrorCodeField) as string | undefined) : undefined) ?? "")
|
|
||||||
: "";
|
|
||||||
if (status !== "Activating") {
|
if (status !== "Activating") {
|
||||||
this.logger.log("Skipping provisioning job: Order not in Activating state", {
|
this.logger.log("Skipping provisioning job: Order not in Activating state", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import { OrderFulfillmentErrorService } from "./order-fulfillment-error.service"
|
|||||||
import { SimFulfillmentService } from "./sim-fulfillment.service";
|
import { SimFulfillmentService } from "./sim-fulfillment.service";
|
||||||
import { DistributedTransactionService } from "@bff/core/database/services/distributed-transaction.service";
|
import { DistributedTransactionService } from "@bff/core/database/services/distributed-transaction.service";
|
||||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
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 OrderSummary, type OrderDetails, type SalesforceOrderRecord, type SalesforceOrderItemRecord } from "@customer-portal/domain/orders";
|
||||||
import type { FulfillmentOrderDetails, FulfillmentOrderItem } from "../types/fulfillment.types";
|
import type { FulfillmentOrderDetails, FulfillmentOrderItem } from "../types/fulfillment.types";
|
||||||
|
|
||||||
@ -45,7 +44,6 @@ export interface OrderFulfillmentContext {
|
|||||||
export class OrderFulfillmentOrchestrator {
|
export class OrderFulfillmentOrchestrator {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(Logger) private readonly logger: Logger,
|
@Inject(Logger) private readonly logger: Logger,
|
||||||
private readonly fieldMapService: SalesforceFieldMapService,
|
|
||||||
private readonly salesforceService: SalesforceService,
|
private readonly salesforceService: SalesforceService,
|
||||||
private readonly whmcsOrderService: WhmcsOrderService,
|
private readonly whmcsOrderService: WhmcsOrderService,
|
||||||
private readonly orderOrchestrator: OrderOrchestrator,
|
private readonly orderOrchestrator: OrderOrchestrator,
|
||||||
@ -135,17 +133,17 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
id: "sf_status_update",
|
id: "sf_status_update",
|
||||||
description: "Update Salesforce order status to Activating",
|
description: "Update Salesforce order status to Activating",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
return await this.salesforceService.updateOrder({
|
return await this.salesforceService.updateOrder({
|
||||||
Id: sfOrderId,
|
Id: sfOrderId,
|
||||||
[fields.order.activationStatus]: "Activating",
|
Activation_Status__c: "Activating",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
rollback: async () => {
|
rollback: async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
await this.salesforceService.updateOrder({
|
await this.salesforceService.updateOrder({
|
||||||
Id: sfOrderId,
|
Id: sfOrderId,
|
||||||
[fields.order.activationStatus]: "Failed",
|
Activation_Status__c: "Failed",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
critical: true,
|
critical: true,
|
||||||
@ -270,20 +268,20 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
id: "sf_success_update",
|
id: "sf_success_update",
|
||||||
description: "Update Salesforce with success",
|
description: "Update Salesforce with success",
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
|
|
||||||
return await this.salesforceService.updateOrder({
|
return await this.salesforceService.updateOrder({
|
||||||
Id: sfOrderId,
|
Id: sfOrderId,
|
||||||
Status: "Completed",
|
Status: "Completed",
|
||||||
[fields.order.activationStatus]: "Activated",
|
Activation_Status__c: "Activated",
|
||||||
[fields.order.whmcsOrderId]: whmcsAcceptResult?.orderId?.toString(),
|
WHMCS_Order_ID__c: whmcsAcceptResult?.orderId?.toString(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
rollback: async () => {
|
rollback: async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
await this.salesforceService.updateOrder({
|
await this.salesforceService.updateOrder({
|
||||||
Id: sfOrderId,
|
Id: sfOrderId,
|
||||||
[fields.order.activationStatus]: "Failed",
|
Activation_Status__c: "Failed",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
critical: true,
|
critical: true,
|
||||||
@ -370,10 +368,10 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
|
|
||||||
// Step 2: Update Salesforce status to "Activating"
|
// Step 2: Update Salesforce status to "Activating"
|
||||||
await this.executeStep(context, "sf_status_update", async () => {
|
await this.executeStep(context, "sf_status_update", async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
await this.salesforceService.updateOrder({
|
await this.salesforceService.updateOrder({
|
||||||
Id: sfOrderId,
|
Id: sfOrderId,
|
||||||
[fields.order.activationStatus]: "Activating",
|
Activation_Status__c: "Activating",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -472,12 +470,12 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
|
|
||||||
// Step 8: Update Salesforce with success
|
// Step 8: Update Salesforce with success
|
||||||
await this.executeStep(context, "sf_success_update", async () => {
|
await this.executeStep(context, "sf_success_update", async () => {
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
await this.salesforceService.updateOrder({
|
await this.salesforceService.updateOrder({
|
||||||
Id: sfOrderId,
|
Id: sfOrderId,
|
||||||
Status: "Completed",
|
Status: "Completed",
|
||||||
[fields.order.activationStatus]: "Activated",
|
Activation_Status__c: "Activated",
|
||||||
[fields.order.whmcsOrderId]: context.whmcsResult!.orderId.toString(),
|
WHMCS_Order_ID__c: context.whmcsResult!.orderId.toString(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -667,7 +665,7 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const errorCode = this.orderFulfillmentErrorService.determineErrorCode(error);
|
const errorCode = this.orderFulfillmentErrorService.determineErrorCode(error);
|
||||||
const userMessage = error.message;
|
const userMessage = error.message;
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
|
|
||||||
this.logger.error("Fulfillment orchestration failed", {
|
this.logger.error("Fulfillment orchestration failed", {
|
||||||
sfOrderId: context.sfOrderId,
|
sfOrderId: context.sfOrderId,
|
||||||
@ -683,14 +681,14 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
Id: context.sfOrderId,
|
Id: context.sfOrderId,
|
||||||
// Set overall Order.Status to Pending Review for manual attention
|
// Set overall Order.Status to Pending Review for manual attention
|
||||||
Status: "Pending Review",
|
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 });
|
await this.salesforceService.updateOrder(updates as { Id: string; [key: string]: unknown });
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { WhmcsPaymentService } from "@bff/integrations/whmcs/services/whmcs-paym
|
|||||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
|
||||||
import { getErrorMessage } from "@bff/core/utils/error.util";
|
import { getErrorMessage } from "@bff/core/utils/error.util";
|
||||||
import type { SalesforceOrderRecord } from "@customer-portal/domain/orders";
|
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";
|
import { sfOrderIdParamSchema } from "@customer-portal/domain/orders";
|
||||||
type OrderStringFieldKey = "activationStatus";
|
type OrderStringFieldKey = "activationStatus";
|
||||||
|
|
||||||
@ -24,7 +23,6 @@ export interface OrderFulfillmentValidationResult {
|
|||||||
export class OrderFulfillmentValidator {
|
export class OrderFulfillmentValidator {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(Logger) private readonly logger: Logger,
|
@Inject(Logger) private readonly logger: Logger,
|
||||||
private readonly fieldMapService: SalesforceFieldMapService,
|
|
||||||
private readonly salesforceService: SalesforceService,
|
private readonly salesforceService: SalesforceService,
|
||||||
private readonly whmcsPaymentService: WhmcsPaymentService,
|
private readonly whmcsPaymentService: WhmcsPaymentService,
|
||||||
private readonly mappingsService: MappingsService
|
private readonly mappingsService: MappingsService
|
||||||
@ -50,9 +48,8 @@ export class OrderFulfillmentValidator {
|
|||||||
const sfOrder = await this.validateSalesforceOrder(sfOrderId);
|
const sfOrder = await this.validateSalesforceOrder(sfOrderId);
|
||||||
|
|
||||||
// 2. Check if already provisioned (idempotency)
|
// 2. Check if already provisioned (idempotency)
|
||||||
const fieldMap = this.fieldMapService.getFieldMap();
|
|
||||||
const rawWhmcs = Reflect.get(sfOrder, fieldMap.order.whmcsOrderId) as unknown;
|
const existingWhmcsOrderId = sfOrder.WHMCS_Order_ID__c;
|
||||||
const existingWhmcsOrderId = typeof rawWhmcs === "string" ? rawWhmcs : undefined;
|
|
||||||
if (existingWhmcsOrderId) {
|
if (existingWhmcsOrderId) {
|
||||||
this.logger.log("Order already provisioned", {
|
this.logger.log("Order already provisioned", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
@ -116,11 +113,11 @@ export class OrderFulfillmentValidator {
|
|||||||
throw new BadRequestException(`Cannot provision cancelled order ${sfOrderId}`);
|
throw new BadRequestException(`Cannot provision cancelled order ${sfOrderId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldMap = this.fieldMapService.getFieldMap();
|
|
||||||
this.logger.log("Salesforce order validated", {
|
this.logger.log("Salesforce order validated", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
status: order.Status,
|
status: order.Status,
|
||||||
activationStatus: pickOrderString(order, "activationStatus", fieldMap),
|
activationStatus: order.Activation_Status__c,
|
||||||
accountId: order.AccountId,
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -109,18 +109,15 @@ export class OrderOrchestrator {
|
|||||||
const safeOrderId = assertSalesforceId(orderId, "orderId");
|
const safeOrderId = assertSalesforceId(orderId, "orderId");
|
||||||
this.logger.log({ orderId: safeOrderId }, "Fetching order details with items");
|
this.logger.log({ orderId: safeOrderId }, "Fetching order details with items");
|
||||||
|
|
||||||
const fieldMap = this.fieldMapService.getFieldMap();
|
const orderQueryFields = buildOrderSelectFields(["Account.Name"]).join(", ");
|
||||||
const orderQueryFields = this.fieldMapService.getOrderQueryFields();
|
const orderItemProduct2Fields = buildOrderItemProduct2Fields().map(f => `PricebookEntry.Product2.${f}`);
|
||||||
const orderItemSelect = [
|
const orderItemSelect = [
|
||||||
this.fieldMapService.getOrderItemQueryFields(),
|
...buildOrderItemSelectFields(),
|
||||||
this.fieldMapService.getOrderItemProduct2Select(),
|
...orderItemProduct2Fields,
|
||||||
]
|
].join(", ");
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const orderSoql = `
|
const orderSoql = `
|
||||||
SELECT ${orderQueryFields}, OrderNumber, TotalAmount,
|
SELECT ${orderQueryFields}
|
||||||
Account.Name, CreatedDate, LastModifiedDate
|
|
||||||
FROM Order
|
FROM Order
|
||||||
WHERE Id = '${safeOrderId}'
|
WHERE Id = '${safeOrderId}'
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@ -155,8 +152,7 @@ export class OrderOrchestrator {
|
|||||||
|
|
||||||
return OrderProviders.Salesforce.transformSalesforceOrderDetails(
|
return OrderProviders.Salesforce.transformSalesforceOrderDetails(
|
||||||
order,
|
order,
|
||||||
orderItems,
|
orderItems
|
||||||
fieldMap
|
|
||||||
);
|
);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.error("Failed to fetch order with items", {
|
this.logger.error("Failed to fetch order with items", {
|
||||||
@ -183,17 +179,15 @@ export class OrderOrchestrator {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldMap = this.fieldMapService.getFieldMap();
|
const orderQueryFields = buildOrderSelectFields().join(", ");
|
||||||
const orderQueryFields = this.fieldMapService.getOrderQueryFields();
|
const orderItemProduct2Fields = buildOrderItemProduct2Fields().map(f => `PricebookEntry.Product2.${f}`);
|
||||||
const orderItemSelect = [
|
const orderItemSelect = [
|
||||||
this.fieldMapService.getOrderItemQueryFields(),
|
...buildOrderItemSelectFields(),
|
||||||
this.fieldMapService.getOrderItemProduct2Select(),
|
...orderItemProduct2Fields,
|
||||||
]
|
].join(", ");
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const ordersSoql = `
|
const ordersSoql = `
|
||||||
SELECT ${orderQueryFields}, OrderNumber, TotalAmount, CreatedDate, LastModifiedDate
|
SELECT ${orderQueryFields}
|
||||||
FROM Order
|
FROM Order
|
||||||
WHERE AccountId = '${sfAccountId}'
|
WHERE AccountId = '${sfAccountId}'
|
||||||
ORDER BY CreatedDate DESC
|
ORDER BY CreatedDate DESC
|
||||||
@ -219,42 +213,52 @@ export class OrderOrchestrator {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderIdsClause = buildInClause(rawOrderIds, "orderIds");
|
const orderIds = rawOrderIds.map(id => assertSalesforceId(id, "orderId"));
|
||||||
|
const inClause = buildInClause(orderIds, "orderIds");
|
||||||
|
|
||||||
const itemsSoql = `
|
const itemsSoql = `
|
||||||
SELECT ${orderItemSelect}
|
SELECT ${orderItemSelect}
|
||||||
FROM OrderItem
|
FROM OrderItem
|
||||||
WHERE OrderId IN (${orderIdsClause})
|
WHERE OrderId IN ${inClause}
|
||||||
ORDER BY OrderId, CreatedDate ASC
|
ORDER BY CreatedDate ASC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const itemsResult = (await this.sf.query(
|
const itemsResult = (await this.sf.query(
|
||||||
itemsSoql
|
itemsSoql
|
||||||
)) as SalesforceQueryResult<SalesforceOrderItemRecord>;
|
)) as SalesforceQueryResult<SalesforceOrderItemRecord>;
|
||||||
const allItems = itemsResult.records ?? [];
|
const allItems = itemsResult.records || [];
|
||||||
|
|
||||||
const itemsByOrder = allItems.reduce<Record<string, SalesforceOrderItemRecord[]>>(
|
// Group items by order ID
|
||||||
(acc, record) => {
|
const itemsByOrder: Record<string, SalesforceOrderItemRecord[]> = {};
|
||||||
const orderId = typeof record.OrderId === "string" ? record.OrderId : undefined;
|
for (const item of allItems) {
|
||||||
if (!orderId) return acc;
|
const orderId = item.OrderId;
|
||||||
if (!acc[orderId]) acc[orderId] = [];
|
if (typeof orderId === "string") {
|
||||||
acc[orderId].push(record);
|
if (!itemsByOrder[orderId]) {
|
||||||
return acc;
|
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
|
return orders
|
||||||
.filter((order): order is SalesforceOrderRecord & { Id: string } => typeof order.Id === "string")
|
.filter((order): order is SalesforceOrderRecord & { Id: string } => typeof order.Id === "string")
|
||||||
.map(order =>
|
.map(order =>
|
||||||
OrderProviders.Salesforce.transformSalesforceOrderSummary(
|
OrderProviders.Salesforce.transformSalesforceOrderSummary(
|
||||||
order,
|
order,
|
||||||
itemsByOrder[order.Id] ?? [],
|
itemsByOrder[order.Id] ?? []
|
||||||
fieldMap
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error: unknown) {
|
} 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),
|
error: getErrorMessage(error),
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
|||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service";
|
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 { z } from "zod";
|
||||||
import type {
|
import type {
|
||||||
SalesforceProduct2Record,
|
SalesforceProduct2Record,
|
||||||
@ -33,7 +31,6 @@ export class OrderPricebookService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly sf: SalesforceConnection,
|
private readonly sf: SalesforceConnection,
|
||||||
private readonly fieldMapService: SalesforceFieldMapService,
|
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
) {}
|
) {}
|
||||||
@ -81,7 +78,7 @@ export class OrderPricebookService {
|
|||||||
return new Map();
|
return new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = this.fieldMapService.getFieldMap();
|
|
||||||
const meta = new Map<string, PricebookProductMeta>();
|
const meta = new Map<string, PricebookProductMeta>();
|
||||||
|
|
||||||
for (let i = 0; i < uniqueSkus.length; i += this.chunkSize) {
|
for (let i = 0; i < uniqueSkus.length; i += this.chunkSize) {
|
||||||
@ -89,13 +86,13 @@ export class OrderPricebookService {
|
|||||||
const whereIn = buildInClause(slice, "skus");
|
const whereIn = buildInClause(slice, "skus");
|
||||||
const soql =
|
const soql =
|
||||||
`SELECT Id, Product2Id, UnitPrice, ` +
|
`SELECT Id, Product2Id, UnitPrice, ` +
|
||||||
`Product2.${fields.product.sku}, ` +
|
`Product2.StockKeepingUnit, ` +
|
||||||
`Product2.${fields.product.itemClass}, ` +
|
`Product2.Item_Class__c, ` +
|
||||||
`Product2.${fields.product.internetOfferingType}, ` +
|
`Product2.Internet_Offering_Type__c, ` +
|
||||||
`Product2.${fields.product.internetPlanTier}, ` +
|
`Product2.Internet_Plan_Tier__c, ` +
|
||||||
`Product2.${fields.product.vpnRegion} ` +
|
`Product2.VPN_Region__c ` +
|
||||||
`FROM PricebookEntry ` +
|
`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 {
|
try {
|
||||||
const res = (await this.sf.query(soql)) as SalesforceQueryResult<
|
const res = (await this.sf.query(soql)) as SalesforceQueryResult<
|
||||||
@ -104,7 +101,7 @@ export class OrderPricebookService {
|
|||||||
|
|
||||||
for (const record of res.records ?? []) {
|
for (const record of res.records ?? []) {
|
||||||
const product = record.Product2 ?? undefined;
|
const product = record.Product2 ?? undefined;
|
||||||
const sku = product ? getStringField(product, "sku", fields) : undefined;
|
const sku = product?.StockKeepingUnit ?? undefined;
|
||||||
if (!sku) continue;
|
if (!sku) continue;
|
||||||
|
|
||||||
const normalizedSku = sku.trim().toUpperCase();
|
const normalizedSku = sku.trim().toUpperCase();
|
||||||
@ -113,14 +110,10 @@ export class OrderPricebookService {
|
|||||||
pricebookEntryId: assertSalesforceId(record.Id ?? undefined, "pricebookEntryId"),
|
pricebookEntryId: assertSalesforceId(record.Id ?? undefined, "pricebookEntryId"),
|
||||||
product2Id: record.Product2Id ?? undefined,
|
product2Id: record.Product2Id ?? undefined,
|
||||||
unitPrice: typeof record.UnitPrice === "number" ? record.UnitPrice : undefined,
|
unitPrice: typeof record.UnitPrice === "number" ? record.UnitPrice : undefined,
|
||||||
itemClass: product ? getStringField(product, "itemClass", fields) : undefined,
|
itemClass: product?.Item_Class__c ?? undefined,
|
||||||
internetOfferingType: product
|
internetOfferingType: product?.Internet_Offering_Type__c ?? undefined,
|
||||||
? getStringField(product, "internetOfferingType", fields)
|
internetPlanTier: product?.Internet_Plan_Tier__c ?? undefined,
|
||||||
: undefined,
|
vpnRegion: product?.VPN_Region__c ?? undefined,
|
||||||
internetPlanTier: product
|
|
||||||
? getStringField(product, "internetPlanTier", fields)
|
|
||||||
: undefined,
|
|
||||||
vpnRegion: product ? getStringField(product, "vpnRegion", fields) : undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -94,88 +94,6 @@ export type OrderType = string;
|
|||||||
*/
|
*/
|
||||||
export type UserMapping = Pick<UserIdMapping, "userId" | "whmcsClientId" | "sfAccountId">;
|
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)
|
// Re-export Types from Schema (Schema-First Approach)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@ -9,15 +9,6 @@
|
|||||||
// Business types
|
// Business types
|
||||||
export { type OrderCreationType, type OrderStatus, type OrderType, type UserMapping } from "./contract";
|
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)
|
// Schemas (includes derived types)
|
||||||
export * from "./schema";
|
export * from "./schema";
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user