Assist_Design/apps/bff/src/orders/services/order-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

206 lines
7.4 KiB
TypeScript

import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { CreateOrderBody, UserMapping } from "../dto/order.dto";
import { getSalesforceFieldMap } from "../../common/config/field-map";
import { UsersService } from "../../users/users.service";
/**
* Handles building order header data from selections
*/
@Injectable()
export class OrderBuilder {
constructor(
@Inject(Logger) private readonly logger: Logger,
private readonly usersService: UsersService
) {}
/**
* Build order fields for Salesforce Order creation
*/
async buildOrderFields(
body: CreateOrderBody,
userMapping: UserMapping,
pricebookId: string,
userId: string
): Promise<Record<string, unknown>> {
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
const fields = getSalesforceFieldMap();
const orderFields: Record<string, unknown> = {
AccountId: userMapping.sfAccountId,
EffectiveDate: today,
Status: "Pending Review",
Pricebook2Id: pricebookId,
[fields.order.orderType]: body.orderType,
...(body.opportunityId ? { OpportunityId: body.opportunityId } : {}),
};
// Add activation fields
this.addActivationFields(orderFields, body);
// Add service-specific fields (only user configuration choices)
switch (body.orderType) {
case "Internet":
this.addInternetFields(orderFields, body);
break;
case "SIM":
this.addSimFields(orderFields, body);
break;
case "VPN":
this.addVpnFields(orderFields, body);
break;
}
// Add address snapshot (single address for both billing and shipping)
await this.addAddressSnapshot(orderFields, userId, body);
return orderFields;
}
private addActivationFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
const fields = getSalesforceFieldMap();
const config = body.configurations || {};
if (config.activationType) {
orderFields[fields.order.activationType] = config.activationType;
}
if (config.scheduledAt) {
orderFields[fields.order.activationScheduledAt] = config.scheduledAt;
}
orderFields[fields.order.activationStatus] = "Not Started";
}
private addInternetFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
const fields = getSalesforceFieldMap();
const config = body.configurations || {};
// Only store user configuration choices that cannot be derived from OrderItems
if (config.accessMode) {
orderFields[fields.order.accessMode] = config.accessMode;
}
// Note: Removed fields that can be derived from OrderItems:
// - internetPlanTier: derive from service product metadata
// - installationType: derive from install product name
// - weekendInstall: derive from SKU analysis
// - hikariDenwa: derive from SKU analysis
}
private addSimFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
const fields = getSalesforceFieldMap();
const config = body.configurations || {};
// Only store user configuration choices that cannot be derived from OrderItems
if (config.simType) {
orderFields[fields.order.simType] = config.simType;
}
if (config.eid) {
orderFields[fields.order.eid] = config.eid;
}
// Note: Removed fields that can be derived from OrderItems:
// - simVoiceMail: derive from SKU analysis
// - simCallWaiting: derive from SKU analysis
// MNP fields
if (config.isMnp === "true") {
orderFields[fields.order.mnp.application] = true;
if (config.mnpNumber) {
orderFields[fields.order.mnp.reservationNumber] = config.mnpNumber;
}
if (config.mnpExpiry) {
orderFields[fields.order.mnp.expiryDate] = config.mnpExpiry;
}
if (config.mnpPhone) {
orderFields[fields.order.mnp.phoneNumber] = config.mnpPhone;
}
if (config.mvnoAccountNumber) {
orderFields[fields.order.mnp.mvnoAccountNumber] = config.mvnoAccountNumber;
}
if (config.portingLastName) {
orderFields[fields.order.mnp.portingLastName] = config.portingLastName;
}
if (config.portingFirstName) {
orderFields[fields.order.mnp.portingFirstName] = config.portingFirstName;
}
if (config.portingLastNameKatakana) {
orderFields[fields.order.mnp.portingLastNameKatakana] = config.portingLastNameKatakana;
}
if (config.portingFirstNameKatakana) {
orderFields[fields.order.mnp.portingFirstNameKatakana] = config.portingFirstNameKatakana;
}
if (config.portingGender) {
orderFields[fields.order.mnp.portingGender] = config.portingGender;
}
if (config.portingDateOfBirth) {
orderFields[fields.order.mnp.portingDateOfBirth] = config.portingDateOfBirth;
}
}
}
private addVpnFields(_orderFields: Record<string, unknown>, _body: CreateOrderBody): void {
// Note: Removed vpnRegion field - can be derived from service product metadata in OrderItems
// VPN orders only need user configuration choices (none currently defined)
}
/**
* Add address snapshot to order
* Always captures current address in billing fields and flags if changed
*/
private async addAddressSnapshot(
orderFields: Record<string, unknown>,
userId: string,
body: CreateOrderBody
): Promise<void> {
try {
const fields = getSalesforceFieldMap();
const billingInfo = await this.usersService.getBillingInfo(userId);
// Check if address was provided/updated in the order request
const orderAddress = (body.configurations as Record<string, unknown>)?.address as
| Record<string, unknown>
| undefined;
const addressChanged = !!orderAddress;
// Use order address if provided, otherwise use current WHMCS address
const addressToUse = orderAddress || billingInfo.address;
// Always populate billing address fields (even if empty)
// Combine street and streetLine2 for Salesforce BillToStreet field
const street = typeof addressToUse?.street === "string" ? addressToUse.street : "";
const streetLine2 =
typeof addressToUse?.streetLine2 === "string" ? addressToUse.streetLine2 : "";
const fullStreet = [street, streetLine2].filter(Boolean).join(", ");
orderFields[fields.order.billing.street] = fullStreet || "";
orderFields[fields.order.billing.city] =
typeof addressToUse?.city === "string" ? addressToUse.city : "";
orderFields[fields.order.billing.state] =
typeof addressToUse?.state === "string" ? addressToUse.state : "";
orderFields[fields.order.billing.postalCode] =
typeof addressToUse?.postalCode === "string" ? addressToUse.postalCode : "";
orderFields[fields.order.billing.country] =
typeof addressToUse?.country === "string" ? addressToUse.country : "";
// Set Address_Changed flag if customer updated address during checkout
orderFields[fields.order.addressChanged] = addressChanged;
if (addressChanged) {
this.logger.log({ userId }, "Customer updated address during checkout");
}
this.logger.debug(
{
userId,
hasAddress: !!street,
addressChanged,
},
"Address snapshot added to order"
);
} catch (error) {
this.logger.error({ userId, error }, "Failed to add address snapshot to order");
// Don't fail the order creation, but log the issue
}
}
}