- 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.
206 lines
7.4 KiB
TypeScript
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
|
|
}
|
|
}
|
|
}
|