Assist_Design/apps/bff/src/orders/services/order-item-builder.service.ts
T. Narantuya 0a387275ff Refactor address handling in AuthService and SignupDto, and enhance order processing with address verification
- Updated AuthService to directly access address fields and added support for address line 2.
- Introduced AddressDto in SignupDto for structured address validation.
- Modified OrdersController to utilize CreateOrderDto for improved type safety.
- Enhanced OrderBuilder to include address snapshot functionality during order creation.
- Updated UsersService to handle address updates and added new methods in WHMCS service for client updates.
- Improved address confirmation logic in AddressConfirmation component for internet orders.
2025-08-29 13:26:57 +09:00

209 lines
6.5 KiB
TypeScript

import { Injectable, BadRequestException, NotFoundException, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { SalesforceConnection } from "../../vendors/salesforce/services/salesforce-connection.service";
import { getSalesforceFieldMap } from "../../common/config/field-map";
// Removed unused import: CreateOrderBody
/**
* Handles building order items from SKU data
*/
@Injectable()
export class OrderItemBuilder {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly sf: SalesforceConnection
) {}
/**
* Create OrderItems directly from SKU array
*/
async createOrderItemsFromSKUs(
orderId: string,
skus: string[],
pricebookId: string
): Promise<void> {
if (skus.length === 0) {
throw new BadRequestException("No products specified for order");
}
this.logger.log({ orderId, skus }, "Creating OrderItems from SKU array");
// Create OrderItems for each SKU
for (const sku of skus) {
const meta = await this.getProductMetaBySku(pricebookId, sku);
if (!meta?.pbeId) {
this.logger.error({ sku }, "PricebookEntry not found for SKU");
throw new NotFoundException(`Product not found: ${sku}`);
}
if (!meta.unitPrice) {
this.logger.error({ sku, pbeId: meta.pbeId }, "PricebookEntry missing UnitPrice");
throw new Error(`PricebookEntry for SKU ${sku} has no UnitPrice set`);
}
this.logger.log(
{
sku,
pbeId: meta.pbeId,
unitPrice: meta.unitPrice,
},
"Creating OrderItem"
);
try {
// Salesforce requires explicit UnitPrice even with PricebookEntryId
await this.sf.sobject("OrderItem").create({
OrderId: orderId,
PricebookEntryId: meta.pbeId,
Quantity: 1,
UnitPrice: meta.unitPrice,
});
this.logger.log({ orderId, sku }, "OrderItem created successfully");
} catch (error) {
this.logger.error({ error, orderId, sku }, "Failed to create OrderItem");
throw error;
}
}
}
/**
* Find Portal pricebook ID
*/
async findPortalPricebookId(): Promise<string> {
const name = process.env.PORTAL_PRICEBOOK_NAME || "Portal";
const soql = `SELECT Id, Name FROM Pricebook2 WHERE IsActive = true AND Name LIKE '%${name}%' LIMIT 1`;
try {
const result = (await this.sf.query(soql)) as { records?: Array<{ Id?: string }> };
if (result.records?.length) {
return result.records[0].Id || "";
}
// fallback to Standard Price Book
const std = (await this.sf.query(
"SELECT Id FROM Pricebook2 WHERE IsStandard = true AND IsActive = true LIMIT 1"
)) as { records?: Array<{ Id?: string }> };
return std.records?.[0]?.Id || "";
} catch (error) {
this.logger.error({ error }, "Failed to find pricebook");
throw new NotFoundException("Portal pricebook not found or inactive");
}
}
/**
* Get product metadata by SKU
*/
async getProductMetaBySku(
pricebookId: string,
sku: string
): Promise<{
pbeId: string;
product2Id: string;
unitPrice?: number;
itemClass?: string;
internetOfferingType?: string;
internetPlanTier?: string;
vpnRegion?: string;
} | null> {
if (!sku) return null;
const fields = getSalesforceFieldMap();
const safeSku = sku.replace(/'/g, "\\'");
const soql =
`SELECT Id, Product2Id, UnitPrice, ` +
`Product2.${fields.product.itemClass}, ` +
`Product2.${fields.product.internetOfferingType}, ` +
`Product2.${fields.product.internetPlanTier}, ` +
`Product2.${fields.product.vpnRegion} ` +
`FROM PricebookEntry ` +
`WHERE Pricebook2Id='${pricebookId}' AND IsActive=true AND Product2.${fields.product.sku}='${safeSku}' ` +
`LIMIT 1`;
try {
this.logger.debug({ sku, pricebookId }, "Querying PricebookEntry for SKU");
const res = (await this.sf.query(soql)) as {
records?: Array<{
Id?: string;
Product2Id?: string;
UnitPrice?: number;
Product2?: Record<string, unknown>;
}>;
};
this.logger.debug(
{
sku,
found: !!res.records?.length,
hasPrice: !!(res.records?.[0] as { UnitPrice?: number })?.UnitPrice,
},
"PricebookEntry query result"
);
const rec = res.records?.[0];
if (!rec?.Id) return null;
return {
pbeId: rec.Id,
product2Id: rec.Product2Id || "",
unitPrice: rec.UnitPrice,
itemClass: (() => {
const value = ((rec as Record<string, unknown>).Product2 as Record<string, unknown>)?.[
fields.product.itemClass
];
return typeof value === "string" ? value : "";
})(),
internetOfferingType: (() => {
const value = ((rec as Record<string, unknown>).Product2 as Record<string, unknown>)?.[
fields.product.internetOfferingType
];
return typeof value === "string" ? value : "";
})(),
internetPlanTier: (() => {
const value = ((rec as Record<string, unknown>).Product2 as Record<string, unknown>)?.[
fields.product.internetPlanTier
];
return typeof value === "string" ? value : "";
})(),
vpnRegion: (() => {
const value = ((rec as Record<string, unknown>).Product2 as Record<string, unknown>)?.[
fields.product.vpnRegion
];
return typeof value === "string" ? value : "";
})(),
};
} catch (error) {
this.logger.error({ error, sku }, "Failed to get product metadata");
return null;
}
}
/**
* Get service product metadata from SKU array for order header defaults
*/
async getServiceProductMetaFromSKUs(skus: string[], pricebookId: string) {
// Find main service SKU (usually first, or contains service indicators)
const serviceSku =
skus.find(
sku =>
!sku.toUpperCase().includes("INSTALL") &&
!sku.toUpperCase().includes("ACTIVATION") &&
!sku.toUpperCase().includes("ADDON")
) || skus[0];
// Find installation SKU
const installSku = skus.find(sku => sku.toUpperCase().includes("INSTALL"));
const [serviceProduct, installProduct] = await Promise.all([
serviceSku ? this.getProductMetaBySku(pricebookId, serviceSku) : null,
installSku ? this.getProductMetaBySku(pricebookId, installSku) : null,
]);
return { serviceProduct, installProduct };
}
}