import { BadRequestException, Injectable, NotFoundException, Inject } from "@nestjs/common"; import { Logger } from "nestjs-pino"; import { SalesforceConnection } from "../vendors/salesforce/services/salesforce-connection.service"; import { MappingsService } from "../mappings/mappings.service"; import { getErrorMessage } from "../common/utils/error.util"; import { WhmcsConnectionService } from "../vendors/whmcs/services/whmcs-connection.service"; interface CreateOrderBody { orderType: "Internet" | "eSIM" | "SIM" | "VPN" | "Other"; selections: Record; opportunityId?: string; } @Injectable() export class OrdersService { constructor( @Inject(Logger) private readonly logger: Logger, private readonly sf: SalesforceConnection, private readonly mappings: MappingsService, private readonly whmcs: WhmcsConnectionService ) {} async create(userId: string, body: CreateOrderBody) { this.logger.log({ userId, orderType: body.orderType }, "Creating order request received"); // 1) Validate mapping const mapping = await this.mappings.findByUserId(userId); if (!mapping?.sfAccountId || !mapping?.whmcsClientId) { this.logger.warn({ userId, mapping }, "Missing SF/WHMCS mapping for user"); throw new BadRequestException("User is not fully linked to Salesforce/WHMCS"); } // 2) Guards: ensure payment method exists and single Internet per account (if Internet) try { // Check client has at least one payment method (best-effort; will be enforced again at provision time) const pay = await this.whmcs.getPayMethods({ clientid: mapping.whmcsClientId }); if ( !pay?.paymethods || !Array.isArray(pay.paymethods.paymethod) || pay.paymethods.paymethod.length === 0 ) { this.logger.warn({ userId }, "No WHMCS payment method on file"); throw new BadRequestException("A payment method is required before ordering"); } } catch (e) { this.logger.warn( { err: getErrorMessage(e) }, "Payment method check soft-failed; proceeding cautiously" ); } if (body.orderType === "Internet") { try { const products = await this.whmcs.getClientsProducts({ clientid: mapping.whmcsClientId }); const existing = products?.products?.product || []; const hasInternet = existing.some((p: any) => String(p.groupname || "") .toLowerCase() .includes("internet") ); if (hasInternet) { throw new BadRequestException("An Internet service already exists for this account"); } } catch (e) { this.logger.warn({ err: getErrorMessage(e) }, "Internet duplicate check soft-failed"); } } // 3) Determine Portal pricebook const pricebook = await this.findPortalPricebookId(); if (!pricebook) { throw new NotFoundException("Portal pricebook not found or inactive"); } // 4) Build Order fields from selections (header) const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD const orderFields: any = { AccountId: mapping.sfAccountId, EffectiveDate: today, Status: "Pending Review", Pricebook2Id: pricebook, Order_Type__c: body.orderType, ...(body.opportunityId ? { OpportunityId: body.opportunityId } : {}), }; // Activation if (body.selections.activationType) orderFields.Activation_Type__c = body.selections.activationType; if (body.selections.scheduledAt) orderFields.Activation_Scheduled_At__c = body.selections.scheduledAt; orderFields.Activation_Status__c = "Not Started"; // Internet config if (body.orderType === "Internet") { if (body.selections.tier) orderFields.Internet_Plan_Tier__c = body.selections.tier; if (body.selections.mode) orderFields.Access_Mode__c = body.selections.mode; if (body.selections.speed) orderFields.Service_Speed__c = body.selections.speed; if (body.selections.install) orderFields.Installment_Plan__c = body.selections.install; if (typeof body.selections.weekend !== "undefined") orderFields.Weekend_Install__c = body.selections.weekend === "true" || body.selections.weekend === true; if (body.selections.install === "12-Month") orderFields.Installment_Months__c = 12; if (body.selections.install === "24-Month") orderFields.Installment_Months__c = 24; } // SIM/eSIM config if (body.orderType === "eSIM" || body.orderType === "SIM") { if (body.selections.simType) orderFields.SIM_Type__c = body.selections.simType === "eSIM" ? "eSIM" : "Physical SIM"; if (body.selections.eid) orderFields.EID__c = body.selections.eid; if (body.selections.isMnp === "true" || body.selections.isMnp === true) { orderFields.MNP_Application__c = true; if (body.selections.mnpNumber) orderFields.MNP_Reservation_Number__c = body.selections.mnpNumber; if (body.selections.mnpExpiry) orderFields.MNP_Expiry_Date__c = body.selections.mnpExpiry; if (body.selections.mnpPhone) orderFields.MNP_Phone_Number__c = body.selections.mnpPhone; } } // 5) Create Order in Salesforce try { const created = await this.sf.sobject("Order").create(orderFields); if (!created?.id) { throw new Error("Salesforce did not return Order Id"); } this.logger.log({ orderId: created.id }, "Salesforce Order created"); // 6) Create OrderItems from header configuration await this.createOrderItems(created.id, body); return { sfOrderId: created.id, status: "Pending Review" }; } catch (error) { this.logger.error( { err: getErrorMessage(error), orderFields }, "Failed to create Salesforce Order" ); throw error; } } async get(userId: string, sfOrderId: string) { try { const soql = `SELECT Id, Status, Activation_Status__c, Activation_Type__c, Activation_Scheduled_At__c, WHMCS_Order_ID__c FROM Order WHERE Id='${sfOrderId}'`; const res = await this.sf.query(soql); if (!res.records?.length) throw new NotFoundException("Order not found"); const o = res.records[0]; return { sfOrderId: o.Id, status: o.Status, activationStatus: o.Activation_Status__c, activationType: o.Activation_Type__c, scheduledAt: o.Activation_Scheduled_At__c, whmcsOrderId: o.WHMCS_Order_ID__c, }; } catch (error) { this.logger.error( { err: getErrorMessage(error), sfOrderId }, "Failed to fetch order summary" ); throw error; } } async provision(userId: string, sfOrderId: string) { this.logger.log({ userId, sfOrderId }, "Provision request received"); // 1) Fetch Order details from Salesforce const soql = `SELECT Id, Status, AccountId, Activation_Type__c, Activation_Scheduled_At__c, Order_Type__c, WHMCS_Order_ID__c FROM Order WHERE Id='${sfOrderId}'`; const res = await this.sf.query(soql); if (!res.records?.length) throw new NotFoundException("Order not found"); const order = res.records[0]; // 2) Validate allowed state if ( order.Status !== "Activated" && order.Status !== "Accepted" && order.Status !== "Pending Review" ) { throw new BadRequestException("Order is not in a provisionable state"); } // 3) Log and return a placeholder; actual WHMCS AddOrder/AcceptOrder will be wired by Flow trigger this.logger.log( { sfOrderId, orderType: order.Order_Type__c }, "Provisioning not yet implemented; placeholder success" ); return { sfOrderId, status: "Accepted", message: "Provisioning queued" }; } private async findPortalPricebookId(): Promise { try { const name = process.env.PORTAL_PRICEBOOK_NAME || "Portal"; const soql = `SELECT Id, Name FROM Pricebook2 WHERE IsActive = true AND Name LIKE '%${name}%' LIMIT 1`; const result = await this.sf.query(soql); 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" ); return std.records?.[0]?.Id || null; } catch (error) { this.logger.error({ err: getErrorMessage(error) }, "Failed to find pricebook"); return null; } } private async findPricebookEntryId( pricebookId: string, product2NameLike: string ): Promise { const soql = `SELECT Id FROM PricebookEntry WHERE Pricebook2Id='${pricebookId}' AND IsActive=true AND Product2.Name LIKE '%${product2NameLike.replace("'", "")}%' LIMIT 1`; const res = await this.sf.query(soql); return res.records?.[0]?.Id || null; } private async findPricebookEntryBySku(pricebookId: string, sku: string): Promise { if (!sku) return null; const skuField = process.env.PRODUCT_SKU_FIELD || "SKU__c"; const safeSku = sku.replace(/'/g, "\\'"); const soql = `SELECT Id FROM PricebookEntry WHERE Pricebook2Id='${pricebookId}' AND IsActive=true AND Product2.${skuField} = '${safeSku}' LIMIT 1`; const res = await this.sf.query(soql); return res.records?.[0]?.Id || null; } private async createOpportunity( accountId: string, body: CreateOrderBody ): Promise { try { const now = new Date(); const name = `${body.orderType} Service ${now.toISOString().slice(0, 10)}`; const opp = await this.sf.sobject("Opportunity").create({ Name: name, AccountId: accountId, StageName: "Qualification", CloseDate: now.toISOString().slice(0, 10), Description: `Created from portal for ${body.orderType}`, }); return opp?.id || null; } catch (e) { this.logger.error({ err: getErrorMessage(e) }, "Failed to create Opportunity"); return null; } } private async createOrderItems(orderId: string, body: CreateOrderBody): Promise { // Minimal SKU resolution using Product2.Name LIKE; in production, prefer Product2 external codes const pricebookId = await this.findPortalPricebookId(); if (!pricebookId) return; const items: Array<{ itemType: string; productHint?: string; sku?: string; billingCycle: string; quantity: number; }> = []; if (body.orderType === "Internet") { // Service line const svcHint = `Internet ${body.selections.tier || ""} ${body.selections.mode || ""}`.trim(); items.push({ itemType: "Service", productHint: svcHint, sku: body.selections.skuService as string | undefined, billingCycle: "monthly", quantity: 1, }); // Installation line const install = body.selections.install as string; if (install === "One-time") { items.push({ itemType: "Installation", productHint: "Installation Fee (Single)", sku: body.selections.skuInstall as string | undefined, billingCycle: "onetime", quantity: 1, }); } else if (install === "12-Month") { items.push({ itemType: "Installation", productHint: "Installation Fee (12-Month)", sku: body.selections.skuInstall as string | undefined, billingCycle: "monthly", quantity: 1, }); } else if (install === "24-Month") { items.push({ itemType: "Installation", productHint: "Installation Fee (24-Month)", sku: body.selections.skuInstall as string | undefined, billingCycle: "monthly", quantity: 1, }); } } else if (body.orderType === "eSIM" || body.orderType === "SIM") { items.push({ itemType: "Service", productHint: `${body.orderType} Plan`, sku: body.selections.skuService as string | undefined, billingCycle: "monthly", quantity: 1, }); } else if (body.orderType === "VPN") { items.push({ itemType: "Service", productHint: `VPN ${body.selections.region || ""}`, sku: body.selections.skuService as string | undefined, billingCycle: "monthly", quantity: 1, }); items.push({ itemType: "Installation", productHint: "VPN Activation Fee", sku: body.selections.skuInstall as string | undefined, billingCycle: "onetime", quantity: 1, }); } for (const it of items) { if (!it.sku) { this.logger.warn({ itemType: it.itemType }, "Missing SKU for order item"); throw new BadRequestException("Missing SKU for order item"); } const pbe = await this.findPricebookEntryBySku(pricebookId, it.sku); if (!pbe) { this.logger.error({ sku: it.sku }, "PricebookEntry not found for SKU"); throw new NotFoundException(`PricebookEntry not found for SKU ${it.sku}`); } await this.sf.sobject("OrderItem").create({ OrderId: orderId, PricebookEntryId: pbe, Quantity: it.quantity, UnitPrice: null, // Salesforce will use the PBE price; null keeps pricebook price Billing_Cycle__c: it.billingCycle.toLowerCase() === "onetime" ? "Onetime" : "Monthly", Item_Type__c: it.itemType, }); } } }