- Refactored the SignupWorkflowService to throw a DomainHttpException for legacy account conflicts, improving error handling. - Updated the SignupForm component to include initialEmail and showFooterLinks props, enhancing user experience during account creation. - Improved the AccountStep in the SignupForm to allow users to add optional details, such as date of birth and gender, for a more personalized signup process. - Enhanced the PasswordStep to include terms acceptance and marketing consent options, ensuring compliance and user engagement. - Updated various catalog views to improve layout and user guidance, streamlining the onboarding process for new users.
230 lines
8.2 KiB
TypeScript
230 lines
8.2 KiB
TypeScript
/**
|
|
* Orders Domain - Salesforce Provider Mapper
|
|
*
|
|
* Transforms Salesforce Order and OrderItem records into domain contracts.
|
|
*/
|
|
|
|
import type {
|
|
OrderDetails,
|
|
OrderItemDetails,
|
|
OrderItemSummary,
|
|
OrderSummary,
|
|
} from "../../contract.js";
|
|
import { normalizeBillingCycle } from "../../helpers.js";
|
|
import { orderDetailsSchema, orderSummarySchema, orderItemDetailsSchema } from "../../schema.js";
|
|
import type {
|
|
SalesforceProduct2WithPricebookEntries,
|
|
SalesforcePricebookEntryRecord,
|
|
} from "../../../catalog/providers/salesforce/raw.types.js";
|
|
import { defaultSalesforceOrderFieldMap, type SalesforceOrderFieldMap } from "./field-map.js";
|
|
import type { SalesforceOrderItemRecord, SalesforceOrderRecord } from "./raw.types.js";
|
|
|
|
/**
|
|
* Helper function to get sort priority for item class
|
|
*/
|
|
function getItemClassSortPriority(itemClass?: string): number {
|
|
if (!itemClass) return 4;
|
|
const normalized = itemClass.toLowerCase();
|
|
if (normalized === "service") return 1;
|
|
if (normalized === "installation" || normalized === "activation") return 2;
|
|
if (normalized === "add-on") return 3;
|
|
return 4;
|
|
}
|
|
|
|
/**
|
|
* Transform a Salesforce OrderItem record into domain details + summary.
|
|
*/
|
|
export function transformSalesforceOrderItem(
|
|
record: SalesforceOrderItemRecord,
|
|
fieldMap: SalesforceOrderFieldMap = defaultSalesforceOrderFieldMap
|
|
): { details: OrderItemDetails; summary: OrderItemSummary } {
|
|
const pricebookEntry = (record.PricebookEntry ?? null) as SalesforcePricebookEntryRecord | null;
|
|
const product = pricebookEntry?.Product2 as SalesforceProduct2WithPricebookEntries | undefined;
|
|
|
|
const orderItemFields = fieldMap.orderItem;
|
|
const productFields = fieldMap.product;
|
|
|
|
const billingCycleRaw =
|
|
record[orderItemFields.billingCycle] ??
|
|
(product ? (product[productFields.billingCycle] as unknown) : undefined);
|
|
const billingCycle =
|
|
billingCycleRaw !== undefined && billingCycleRaw !== null
|
|
? normalizeBillingCycle(billingCycleRaw)
|
|
: undefined;
|
|
|
|
const details = orderItemDetailsSchema.parse({
|
|
id: record.Id,
|
|
orderId: ensureString(record.OrderId) ?? "",
|
|
quantity: normalizeQuantity(record.Quantity),
|
|
unitPrice: coerceNumber(record.UnitPrice),
|
|
totalPrice: coerceNumber(record.TotalPrice),
|
|
billingCycle,
|
|
product: product
|
|
? {
|
|
id: ensureString(product.Id),
|
|
name: ensureString(product.Name),
|
|
sku: ensureString(product[productFields.sku]) ?? undefined,
|
|
itemClass: ensureString(product[productFields.itemClass]) ?? undefined,
|
|
whmcsProductId: resolveWhmcsProductId(product[productFields.whmcsProductId]),
|
|
internetOfferingType:
|
|
ensureString(product[productFields.internetOfferingType]) ?? undefined,
|
|
internetPlanTier: ensureString(product[productFields.internetPlanTier]) ?? undefined,
|
|
vpnRegion: ensureString(product[productFields.vpnRegion]) ?? undefined,
|
|
isBundledAddon: coerceBoolean(product[productFields.isBundledAddon]),
|
|
bundledAddonId: ensureString(product[productFields.bundledAddon]) ?? undefined,
|
|
}
|
|
: undefined,
|
|
});
|
|
|
|
return {
|
|
details,
|
|
summary: {
|
|
productName: details.product?.name,
|
|
name: details.product?.name,
|
|
sku: details.product?.sku,
|
|
productId: details.product?.id,
|
|
status: undefined,
|
|
billingCycle: details.billingCycle,
|
|
itemClass: details.product?.itemClass,
|
|
quantity: details.quantity,
|
|
unitPrice: details.unitPrice,
|
|
totalPrice: details.totalPrice,
|
|
isBundledAddon: details.product?.isBundledAddon,
|
|
bundledAddonId: details.product?.bundledAddonId,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform a Salesforce Order record (with associated OrderItems) into domain OrderDetails.
|
|
*/
|
|
export function transformSalesforceOrderDetails(
|
|
order: SalesforceOrderRecord,
|
|
itemRecords: SalesforceOrderItemRecord[],
|
|
fieldMap: SalesforceOrderFieldMap = defaultSalesforceOrderFieldMap
|
|
): OrderDetails {
|
|
const transformedItems = itemRecords.map(record =>
|
|
transformSalesforceOrderItem(record, fieldMap)
|
|
);
|
|
|
|
// Sort items by item class priority (Service -> Installation/Activation -> Add-on -> Others)
|
|
transformedItems.sort((a, b) => {
|
|
const priorityA = getItemClassSortPriority(a.summary.itemClass);
|
|
const priorityB = getItemClassSortPriority(b.summary.itemClass);
|
|
return priorityA - priorityB;
|
|
});
|
|
|
|
const items = transformedItems.map(item => item.details);
|
|
const itemsSummary = transformedItems.map(item => item.summary);
|
|
|
|
const summary = buildOrderSummary(order, itemsSummary, fieldMap);
|
|
const orderFields = fieldMap.order;
|
|
|
|
return orderDetailsSchema.parse({
|
|
...summary,
|
|
accountId: ensureString(order.AccountId),
|
|
accountName: ensureString(order.Account?.Name),
|
|
pricebook2Id: ensureString(order.Pricebook2Id),
|
|
opportunityId: ensureString(order.OpportunityId), // Linked Opportunity for lifecycle tracking
|
|
activationType: ensureString(order[orderFields.activationType]),
|
|
activationStatus: summary.activationStatus,
|
|
activationScheduledAt: ensureString(order[orderFields.activationScheduledAt]),
|
|
activationErrorCode: ensureString(order[orderFields.activationErrorCode]),
|
|
activationErrorMessage: ensureString(order[orderFields.activationErrorMessage]),
|
|
activatedDate: ensureString(order.ActivatedDate),
|
|
items,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Transform a Salesforce Order record (with optional OrderItems) into domain OrderSummary.
|
|
*/
|
|
export function transformSalesforceOrderSummary(
|
|
order: SalesforceOrderRecord,
|
|
itemRecords: SalesforceOrderItemRecord[],
|
|
fieldMap: SalesforceOrderFieldMap = defaultSalesforceOrderFieldMap
|
|
): OrderSummary {
|
|
const transformedItems = itemRecords.map(record =>
|
|
transformSalesforceOrderItem(record, fieldMap)
|
|
);
|
|
|
|
// Sort items by item class priority (Service -> Installation/Activation -> Add-on -> Others)
|
|
transformedItems.sort((a, b) => {
|
|
const priorityA = getItemClassSortPriority(a.summary.itemClass);
|
|
const priorityB = getItemClassSortPriority(b.summary.itemClass);
|
|
return priorityA - priorityB;
|
|
});
|
|
|
|
const itemsSummary = transformedItems.map(item => item.summary);
|
|
return buildOrderSummary(order, itemsSummary, fieldMap);
|
|
}
|
|
|
|
function buildOrderSummary(
|
|
order: SalesforceOrderRecord,
|
|
itemsSummary: OrderItemSummary[],
|
|
fieldMap: SalesforceOrderFieldMap
|
|
): OrderSummary {
|
|
const orderFields = fieldMap.order;
|
|
const effectiveDate =
|
|
ensureString(order.EffectiveDate) ??
|
|
ensureString(order.CreatedDate) ??
|
|
new Date().toISOString();
|
|
const createdDate = ensureString(order.CreatedDate) ?? effectiveDate;
|
|
const lastModifiedDate = ensureString(order.LastModifiedDate) ?? createdDate;
|
|
const totalAmount = coerceNumber(order.TotalAmount);
|
|
|
|
return orderSummarySchema.parse({
|
|
id: order.Id,
|
|
orderNumber: ensureString(order.OrderNumber) ?? order.Id,
|
|
status: ensureString(order.Status) ?? "Unknown",
|
|
orderType: ensureString(order[orderFields.type]) ?? undefined,
|
|
effectiveDate,
|
|
totalAmount: typeof totalAmount === "number" ? totalAmount : undefined,
|
|
createdDate,
|
|
lastModifiedDate,
|
|
whmcsOrderId: ensureString(order[orderFields.whmcsOrderId]) ?? undefined,
|
|
activationStatus: ensureString(order[orderFields.activationStatus]) ?? undefined,
|
|
itemsSummary,
|
|
});
|
|
}
|
|
|
|
function ensureString(value: unknown): string | undefined {
|
|
return typeof value === "string" ? value : undefined;
|
|
}
|
|
|
|
function coerceNumber(value: unknown): number | undefined {
|
|
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
|
|
if (typeof value === "string") {
|
|
const parsed = Number.parseFloat(value);
|
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeQuantity(value: unknown): number {
|
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
return Math.trunc(value);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
function resolveWhmcsProductId(value: unknown): string | undefined {
|
|
if (value === null || value === undefined) {
|
|
return undefined;
|
|
}
|
|
if (typeof value === "number") {
|
|
return Number.isFinite(value) ? String(value) : undefined;
|
|
}
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function coerceBoolean(value: unknown): boolean | undefined {
|
|
if (typeof value === "boolean") {
|
|
return value;
|
|
}
|
|
return undefined;
|
|
}
|