diff --git a/server/rr/rr-helpers.js b/server/rr/rr-helpers.js index 8f0a59ff4..58c8fc199 100644 --- a/server/rr/rr-helpers.js +++ b/server/rr/rr-helpers.js @@ -6,6 +6,8 @@ * - Parses XML response (faults + STAR payload result) */ +"use strict"; + const fs = require("fs/promises"); const path = require("path"); const axios = require("axios"); @@ -68,7 +70,7 @@ async function loadTemplate(templateName) { async function renderXmlTemplate(templateName, data) { const tpl = await loadTemplate(templateName); // Render and strip any XML declaration to keep a single root element for the BOD - const rendered = mustache.render(tpl, data || {}); + const rendered = mustache.render(tpl, { STAR_NS: RR_NS.STAR_BUSINESS, ...(data || {}) }); return rendered.replace(/^\s*<\?xml[^>]*\?>\s*/i, ""); } @@ -202,12 +204,26 @@ function wrapWithApplicationArea(innerXml, { CreationDateTime, BODId, Sender, De return xml; } -async function buildStarEnvelope(innerBusinessXml, creds, appArea = {}) { +const TASK_BY_ACTION = { + CreateRepairOrder: { Task: "BSMRO", ReferenceId: "Insert" }, + UpdateRepairOrder: { Task: "BSMRO", ReferenceId: "Update" }, + InsertCustomer: { Task: "CU", ReferenceId: "Insert" }, + UpdateCustomer: { Task: "CU", ReferenceId: "Update" }, + InsertServiceVehicle: { Task: "SV", ReferenceId: "Insert" } +}; + +async function buildStarEnvelope(innerBusinessXml, creds, appArea = {}, action) { const now = new Date().toISOString(); + + // Derive sensible defaults for Sender from action, unless caller provided explicit values + const senderDefaults = + appArea?.Sender || + (action && TASK_BY_ACTION[action] ? { Component: "Rome", ...TASK_BY_ACTION[action] } : { Component: "Rome" }); + const payloadWithAppArea = wrapWithApplicationArea(innerBusinessXml, { CreationDateTime: appArea.CreationDateTime || now, BODId: appArea.BODId || `BOD-${Date.now()}`, - Sender: appArea.Sender || { Component: "Rome", Task: "SV", ReferenceId: "Update" }, + Sender: senderDefaults, Destination: appArea.Destination || { DealerNumber: creds.dealerNumber, StoreNumber: String(creds.storeNumber ?? "").padStart(2, "0"), @@ -241,6 +257,7 @@ async function buildStarEnvelope(innerBusinessXml, creds, appArea = {}) { async function MakeRRCall({ action, body, + appArea, // <-- allow explicit ApplicationArea overrides at the top level socket, dealerConfig, // required in runtime code; rr-test.js can still pass env-inflated cfg retries = 1, @@ -259,8 +276,9 @@ async function MakeRRCall({ const templateName = body?.template || action; const renderedBusiness = await renderXmlTemplate(templateName, body?.data || {}); - // Build STAR envelope - const envelope = await buildStarEnvelope(renderedBusiness, cfg, body?.appArea); + // Build STAR envelope (use explicit appArea if provided; else accept body.appArea; else derive from action) + const selectedAppArea = appArea || body?.appArea || {}; + const envelope = await buildStarEnvelope(renderedBusiness, cfg, selectedAppArea, action); const formattedEnvelope = prettyPrintXml(envelope); // Guardrails @@ -310,6 +328,7 @@ async function MakeRRCall({ return MakeRRCall({ action, body, + appArea: selectedAppArea, socket, dealerConfig: cfg, retries: retries - 1, diff --git a/server/rr/rr-mappers.js b/server/rr/rr-mappers.js index b986075b9..601f62019 100644 --- a/server/rr/rr-mappers.js +++ b/server/rr/rr-mappers.js @@ -32,6 +32,22 @@ function getDSB(cfg) { return { dealerNumber, storeNumber, branchNumber }; } +/** + * Normalize an address-like object to the template's
block. + */ +function mapAddress(addr) { + if (!addr) return undefined; + const out = { + Line1: addr.line1, + Line2: addr.line2, + City: addr.city, + State: addr.state, + PostalCode: addr.postal_code || addr.postalCode, + Country: addr.country + }; + return hasAny(out) ? out : undefined; +} + // // ===================== CUSTOMER ===================== // @@ -39,71 +55,36 @@ function getDSB(cfg) { /** * Map internal customer record to Rome CustomerInsertRq. */ -function mapCustomerInsert(customer, bodyshopConfig) { - if (!customer) return {}; - const { dealerNumber, storeNumber, branchNumber } = getDSB(bodyshopConfig); - +function mapCustomerInsert(src) { + const name = src.company_name?.trim() || [src.first_name, src.last_name].filter(Boolean).join(" ").trim(); return { - DealerCode: bodyshopConfig?.dealer_code || "ROME", - DealerNumber: dealerNumber, - StoreNumber: storeNumber, - BranchNumber: branchNumber, - RequestId: `CUST-INSERT-${customer.id}`, - Environment: process.env.NODE_ENV, - - CustomerId: customer.external_id || undefined, - CustomerType: customer.type || "RETAIL", - CompanyName: customer.company_name, - FirstName: customer.first_name, - MiddleName: customer.middle_name, - LastName: customer.last_name, - PreferredName: customer.display_name || customer.first_name, - ActiveFlag: customer.active ? "true" : "false", - - CustomerGroup: customer.group_name, - TaxExempt: customer.tax_exempt ? "true" : "false", - DiscountLevel: num(customer.discount_level), - PreferredLanguage: customer.language || "EN", - - Addresses: (customer.addresses || []).map((a) => ({ - AddressType: a.type || "BILLING", - AddressLine1: a.line1, - AddressLine2: a.line2, + CustomerNumber: src.external_id, // optional + CustomerType: src.type === "BUSINESS" ? "BUSINESS" : "RETAIL", + CustomerName: name, + DisplayName: src.display_name || name, + Language: src.language || "EN", + TaxExempt: src.tax_exempt ? "Y" : "N", + Active: src.active ? "Y" : "N", + Addresses: (src.addresses || []).map((a) => ({ + Type: a.type || "P", + Line1: a.line1, + Line2: a.line2, City: a.city, State: a.state, PostalCode: a.postal_code, - Country: a.country || "US" + Country: a.country })), - - Phones: (customer.phones || []).map((p) => ({ - PhoneNumber: p.number, - PhoneType: p.type || "MOBILE", - Preferred: p.preferred ? "true" : "false" + Phones: (src.phones || []).map((p) => ({ + Type: p.type || "H", + Number: p.number, + Extension: p.extension, + Preferred: p.preferred ? "Y" : "N" })), - - Emails: (customer.emails || []).map((e) => ({ - EmailAddress: e.address, - EmailType: e.type || "WORK", - Preferred: e.preferred ? "true" : "false" - })), - - Insurance: customer.insurance - ? { - CompanyName: customer.insurance.company, - PolicyNumber: customer.insurance.policy, - ExpirationDate: formatDate(customer.insurance.expiration_date), - ContactName: customer.insurance.contact_name, - ContactPhone: customer.insurance.contact_phone - } - : undefined, - - LinkedAccounts: (customer.linked_accounts || []).map((a) => ({ - Type: a.type, - AccountNumber: a.account_number, - CreditLimit: num(a.credit_limit) - })), - - Notes: customer.notes?.length ? { Items: customer.notes.map((n) => n.text || n) } : undefined + Emails: (src.emails || []).map((e) => ({ + Type: e.type || "W", + Address: e.address, + Preferred: e.preferred ? "Y" : "N" + })) }; } @@ -184,6 +165,8 @@ function mapServiceVehicle(vehicle, ownerCustomer, bodyshopConfig) { /** * Map internal job to Rome RepairOrderInsertRq. + * NOTE: The CreateRepairOrder.xml template expects *flat* fields for Customer and ServiceVehicle + * (no {{#Customer}} or {{#ServiceVehicle}} sections). Therefore, we flatten those values here. */ function mapRepairOrderCreate(job, bodyshopConfig) { if (!job) return {}; @@ -192,7 +175,11 @@ function mapRepairOrderCreate(job, bodyshopConfig) { const cust = job.customer || {}; const veh = job.vehicle || {}; + // Prefer a concrete address on the customer, fall back to job-level + const customerAddress = cust.address || job.customer_address || job.address || undefined; + return { + // Routing/meta we keep available for logging or other templates DealerCode: bodyshopConfig?.dealer_code || "ROME", DealerNumber: dealerNumber, StoreNumber: storeNumber, @@ -200,6 +187,7 @@ function mapRepairOrderCreate(job, bodyshopConfig) { RequestId: `RO-${job.id}`, Environment: process.env.NODE_ENV, + // Header fields RepairOrderNumber: job.ro_number, DmsRepairOrderId: job.external_id, @@ -215,26 +203,26 @@ function mapRepairOrderCreate(job, bodyshopConfig) { ROType: job.ro_type, Status: job.status, IsBodyShop: "true", - DRPFlag: job.drp_flag ? "true" : "false", + DRPFlag: toBoolStr(!!job.drp_flag) || "false", - Customer: { - CustomerId: cust.external_id, - CustomerName: cust.full_name, - PhoneNumber: cust.phone, - EmailAddress: cust.email - }, + // Customer block is FLAT (template does not use {{#Customer}} section) + CustomerId: cust.external_id, + CustomerName: cust.full_name || [cust.first_name, cust.last_name].filter(Boolean).join(" ").trim() || undefined, + PhoneNumber: cust.phone, + EmailAddress: cust.email, + Address: mapAddress(customerAddress), - Vehicle: { - VehicleId: veh.external_id, - VIN: veh.vin, - LicensePlate: veh.license_plate, - Year: num(veh.year), - Make: veh.make, - Model: veh.model, - Odometer: num(veh.odometer), - Color: veh.color - }, + // ServiceVehicle block is FLAT (template does not use {{#ServiceVehicle}} section) + VehicleId: veh.external_id, + VIN: veh.vin, + LicensePlate: veh.license_plate, + Year: num(veh.year), + Make: veh.make, + Model: veh.model, + Odometer: num(veh.odometer), + Color: veh.color, + // Lines JobLines: (job.joblines || []).map((l, i) => ({ Sequence: i + 1, ParentSequence: l.parent_sequence, @@ -264,16 +252,28 @@ function mapRepairOrderCreate(job, bodyshopConfig) { : undefined })), - Totals: { + // Totals + Totals: hasAny({ Currency: job.currency || "CAD", - LaborTotal: num(job.totals?.labor), - PartsTotal: num(job.totals?.parts), - MiscTotal: num(job.totals?.misc), - DiscountTotal: num(job.totals?.discount), - TaxTotal: num(job.totals?.tax), - GrandTotal: num(job.totals?.grand) - }, + LaborTotal: job.totals?.labor, + PartsTotal: job.totals?.parts, + MiscTotal: job.totals?.misc, + DiscountTotal: job.totals?.discount, + TaxTotal: job.totals?.tax, + GrandTotal: job.totals?.grand + }) + ? { + Currency: job.currency || "CAD", + LaborTotal: num(job.totals?.labor), + PartsTotal: num(job.totals?.parts), + MiscTotal: num(job.totals?.misc), + DiscountTotal: num(job.totals?.discount), + TaxTotal: num(job.totals?.tax), + GrandTotal: num(job.totals?.grand) + } + : undefined, + // Payments Payments: job.payments?.length ? { Items: job.payments.map((p) => ({ @@ -287,6 +287,7 @@ function mapRepairOrderCreate(job, bodyshopConfig) { } : undefined, + // Insurance Insurance: job.insurance ? { CompanyName: job.insurance.company, @@ -296,6 +297,7 @@ function mapRepairOrderCreate(job, bodyshopConfig) { } : undefined, + // Notes Notes: job.notes?.length ? { Items: job.notes.map((n) => n.text || n) } : undefined }; } diff --git a/server/rr/rr-repair-orders.js b/server/rr/rr-repair-orders.js index fad0a3fc3..8a7f66107 100644 --- a/server/rr/rr-repair-orders.js +++ b/server/rr/rr-repair-orders.js @@ -4,11 +4,63 @@ * Handles creation and updates of repair orders (BSMRepairOrderRq/Resp). */ +"use strict"; + const { MakeRRCall } = require("./rr-helpers"); const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers"); const RRLogger = require("./rr-logger"); const { RrApiError } = require("./rr-error"); +/** + * Very light sanity checks before we build XML. + * We keep these minimal because the mapper may derive some fields. + * Throws RrApiError when a required precondition is missing. + * @param {"CreateRepairOrder"|"UpdateRepairOrder"} action + * @param {Object} job + */ +function preflight(action, job) { + if (!job || !job.id) { + throw new RrApiError("Missing job payload or job.id", "RR_BAD_JOB_PAYLOAD"); + } + // VIN is almost always required for BSM RO flows + const vin = job?.vehicle?.vin || job?.vehicle?.VIN || job?.VIN || job?.vin; + if (!vin) { + throw new RrApiError("Missing VIN on job.vehicle", "RR_MISSING_VIN"); + } + + if (action === "UpdateRepairOrder") { + // If your mapper expects a DMS RO number or an external RO number, + // you can tighten this guard based on your schema, e.g.: + // const hasKey = job?.dms_ro_no || job?.external_ro_number || job?.roNumber; + // if (!hasKey) throw new RrApiError("Missing RO key for update", "RR_MISSING_RO_KEY"); + } +} + +/** + * Build an explicit ApplicationArea override so the envelope is always correct, + * even if the helper falls back to defaults. We include routing info + * and set Task/ReferenceId for BSM repair orders. + * @param {"CreateRepairOrder"|"UpdateRepairOrder"} action + * @param {Object} cfg + */ +function buildAppArea(action, cfg) { + const isCreate = action === "CreateRepairOrder"; + return { + Sender: { + Component: "Rome", + Task: "BSMRO", + ReferenceId: isCreate ? "Insert" : "Update" + }, + Destination: { + DealerNumber: cfg?.DealerNumber || cfg?.dealerNumber, + StoreNumber: cfg?.StoreNumber || cfg?.storeNumber, + AreaNumber: cfg?.AreaNumber || cfg?.areaNumber, + DestinationNameCode: "RR" + } + // CreationDateTime and BODId will be provided by rr-helpers if omitted. + }; +} + /** * Create a new repair order in Rome. * @param {Socket} socket - active socket connection @@ -21,13 +73,20 @@ async function createRepairOrder(socket, job, bodyshopConfig) { const template = "CreateRepairOrder"; // maps to xml-templates/CreateRepairOrder.xml try { - RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`); + RRLogger(socket, "info", `Starting RR ${action} for job ${job?.id}`, { + jobid: job?.id, + dealer: bodyshopConfig?.DealerNumber || bodyshopConfig?.dealerNumber, + store: bodyshopConfig?.StoreNumber || bodyshopConfig?.storeNumber, + area: bodyshopConfig?.AreaNumber || bodyshopConfig?.areaNumber + }); + preflight(action, job); const data = mapRepairOrderCreate(job, bodyshopConfig); const resultXml = await MakeRRCall({ action, body: { template, data }, + appArea: buildAppArea(action, bodyshopConfig), socket, dealerConfig: bodyshopConfig, jobid: job.id @@ -43,9 +102,9 @@ async function createRepairOrder(socket, job, bodyshopConfig) { xml: resultXml }; } catch (error) { - RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, { - message: error.message, - stack: error.stack + RRLogger(socket, "error", `Error in ${action} for job ${job?.id}`, { + message: error?.message, + stack: error?.stack }); throw new RrApiError(`RR CreateRepairOrder failed: ${error.message}`, "CREATE_RO_ERROR"); } @@ -63,13 +122,20 @@ async function updateRepairOrder(socket, job, bodyshopConfig) { const template = "UpdateRepairOrder"; try { - RRLogger(socket, "info", `Starting RR ${action} for job ${job.id}`); + RRLogger(socket, "info", `Starting RR ${action} for job ${job?.id}`, { + jobid: job?.id, + dealer: bodyshopConfig?.DealerNumber || bodyshopConfig?.dealerNumber, + store: bodyshopConfig?.StoreNumber || bodyshopConfig?.storeNumber, + area: bodyshopConfig?.AreaNumber || bodyshopConfig?.areaNumber + }); + preflight(action, job); const data = mapRepairOrderUpdate(job, bodyshopConfig); const resultXml = await MakeRRCall({ action, body: { template, data }, + appArea: buildAppArea(action, bodyshopConfig), socket, dealerConfig: bodyshopConfig, jobid: job.id @@ -85,9 +151,9 @@ async function updateRepairOrder(socket, job, bodyshopConfig) { xml: resultXml }; } catch (error) { - RRLogger(socket, "error", `Error in ${action} for job ${job.id}`, { - message: error.message, - stack: error.stack + RRLogger(socket, "error", `Error in ${action} for job ${job?.id}`, { + message: error?.message, + stack: error?.stack }); throw new RrApiError(`RR UpdateRepairOrder failed: ${error.message}`, "UPDATE_RO_ERROR"); } diff --git a/server/rr/rr-test.js b/server/rr/rr-test.js index a032cf01e..6a677efa0 100644 --- a/server/rr/rr-test.js +++ b/server/rr/rr-test.js @@ -1,265 +1,484 @@ #!/usr/bin/env node /** - * RR smoke test / CLI (STAR-only) + * rr-test.js — end-to-end exerciser for Reynolds "Rome"/STAR actions. + * + * Key improvements vs prior version: + * - Prints FULL XML responses (no truncation). + * - CombinedSearch now sends at least one search criterion by default. + * - InsertCustomer sets required IBFlag + LastName/FirstName. + * - Orchestrates dependent steps (CombinedSearch → InsertCustomer → InsertVehicle → Create/Update RO). + * + * Usage examples: + * node rr-test.js --ping + * node rr-test.js --all + * node rr-test.js --combined --last "SMITH" --phone 9375550001 --vin 1FTFW1E50JFA00000 + * + * Common flags: + * --first, --last, --phone, --email, --vin, --plate, --advisor, --max N + * + * Env fallbacks: + * RR_TEST_LAST, RR_TEST_FIRST, RR_TEST_PHONE, RR_TEST_EMAIL, RR_TEST_VIN, RR_TEST_PLATE, RR_TEST_PARTDESC + * + * Dealer creds/env (used only in this test harness; runtime pulls from DB): + * RR_DEALER_NUMBER, RR_STORE_NUMBER, RR_AREA_NUMBER, RR_USERNAME, RR_PASSWORD, RR_ENDPOINT */ const path = require("path"); const fs = require("fs"); -const dotenv = require("dotenv"); -const { GraphQLClient, gql } = require("graphql-request"); -const { MakeRRCall, renderXmlTemplate, buildStarEnvelope } = require("./rr-helpers"); -const { getBaseRRConfig } = require("./rr-constants"); +const fsp = require("fs/promises"); +const minimist = require("minimist"); +const dayjs = require("dayjs"); +const { XMLParser } = require("fast-xml-parser"); -// Load env file for local runs -const defaultEnvPath = path.resolve(__dirname, "../../.env.development"); -if (fs.existsSync(defaultEnvPath)) { - const result = dotenv.config({ path: defaultEnvPath }); - if (result?.parsed) { - console.log( - `${defaultEnvPath}\n[dotenv@${require("dotenv/package.json").version}] injecting env (${Object.keys(result.parsed).length}) from ../../.env.development` - ); +// Load dev env if present +const envPath = path.resolve(__dirname, "../../.env.development"); +if (fs.existsSync(envPath)) { + require("dotenv").config({ path: envPath }); + console.log(envPath); + console.log( + `[dotenv@${require("dotenv/package.json").version}] injecting env (${Object.keys(process.env).length}) from ../../.env.development` + ); +} + +const { MakeRRCall, parseRRResponse } = require("./rr-helpers"); +const RRLogger = require("./rr-logger"); + +// CLI flags +const argv = minimist(process.argv.slice(2)); +const FLAG = (k, d) => (argv[k] !== undefined ? argv[k] : d); + +// For templates that expect STAR NS in {{STAR_NS}} +const STAR_NS = "http://www.starstandards.org/STAR"; + +// Basic XML parser for optional extractions +const parser = new XMLParser({ ignoreAttributes: false, removeNSPrefix: true }); + +// Ensure templates exist +async function verifyTemplates() { + const required = [ + "CombinedSearch", + "GetAdvisors", + "GetParts", + "InsertCustomer", + "UpdateCustomer", + "InsertServiceVehicle", + "CreateRepairOrder", + "UpdateRepairOrder" + ]; + for (const t of required) { + const p = path.join(__dirname, "xml-templates", `${t}.xml`); + await fsp.readFile(p, "utf8"); + } + console.log("✅ Templates verified."); +} + +// Format AppArea per action (Sender.Task/ReferenceId) +function appAreaFor(action, cfg) { + const pad2 = (x) => String(x ?? "").padStart(2, "0"); + const base = { + CreationDateTime: new Date().toISOString(), + BODId: `BOD-${Date.now()}`, + Sender: { Component: "Rome", CreatorNameCode: "RCI", SenderNameCode: "RCI" }, + Destination: { + DealerNumber: cfg.dealerNumber, + StoreNumber: pad2(cfg.storeNumber), + AreaNumber: pad2(cfg.branchNumber || "01") + } + }; + switch (action) { + case "CreateRepairOrder": + return { ...base, Sender: { ...base.Sender, Task: "BSMRO", ReferenceId: "Insert" } }; + case "UpdateRepairOrder": + return { ...base, Sender: { ...base.Sender, Task: "BSMRO", ReferenceId: "Update" } }; + case "InsertCustomer": + return { ...base, Sender: { ...base.Sender, Task: "CU", ReferenceId: "Insert" } }; + case "UpdateCustomer": + return { ...base, Sender: { ...base.Sender, Task: "CU", ReferenceId: "Update" } }; + case "InsertServiceVehicle": + return { ...base, Sender: { ...base.Sender, Task: "SV", ReferenceId: "Insert" } }; + case "CombinedSearch": + return { ...base, Sender: { ...base.Sender, Task: "SV", ReferenceId: "Query" } }; + case "GetAdvisors": + return { ...base, Sender: { ...base.Sender, Task: "RCI", ReferenceId: "Lookup" } }; + case "GetParts": + return { ...base, Sender: { ...base.Sender, Task: "RCI", ReferenceId: "Lookup" } }; + default: + return base; } } -// ---- CLI args parsing ---- -const argv = process.argv.slice(2); -const args = { _: [] }; -for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - - if (a.startsWith("--")) { - const eq = a.indexOf("="); - if (eq > -1) { - const k = a.slice(2, eq); - const v = a.slice(eq + 1); - args[k] = v; - } else { - const k = a.slice(2); - const next = argv[i + 1]; - if (next && !next.startsWith("-")) { - args[k] = next; - i++; - } else { - args[k] = true; - } - } - } else if (a.startsWith("-") && a.length > 1) { - const k = a.slice(1); - const next = argv[i + 1]; - if (next && !next.startsWith("-")) { - args[k] = next; - i++; - } else { - args[k] = true; - } - } else { - args._.push(a); - } -} - -function toIntOr(defaultVal, maybe) { - const n = parseInt(maybe, 10); - return Number.isFinite(n) ? n : defaultVal; -} - -// ---------------- GraphQL helpers ---------------- - -function buildGqlClient() { - const endpoint = process.env.GRAPHQL_ENDPOINT; - if (!endpoint) throw new Error("GRAPHQL_ENDPOINT env var is required when using --bodyshopId."); - - const headers = {}; - if (process.env.HASURA_ADMIN_SECRET) { - headers["x-hasura-admin-secret"] = process.env.HASURA_ADMIN_SECRET; - } else if (process.env.GRAPHQL_BEARER) { - headers["authorization"] = `Bearer ${process.env.GRAPHQL_BEARER}`; - } - - return new GraphQLClient(endpoint, { headers }); -} - -const Q_BODYSHOPS_BY_PK = gql` - query BodyshopRR($id: uuid!) { - bodyshops_by_pk(id: $id) { - rr_dealerid - rr_configuration - } - } -`; - -function normalizeConfigRecord(rec) { - if (!rec) return null; - let cfg = rec.rr_configuration || {}; - if (typeof cfg === "string") { - try { - cfg = JSON.parse(cfg); - } catch { - cfg = {}; - } - } +// Build cfg for test harness (env fallbacks allowed here) +function cfgFromEnv() { return { - dealerNumber: rec.rr_dealerid || undefined, - storeNumber: cfg.storeNumber || undefined, - branchNumber: cfg.branchNumber || undefined + baseUrl: + process.env.RR_ENDPOINT || process.env.RR_BASE_URL || "https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx", + username: process.env.RR_USERNAME || "Rome", + password: process.env.RR_PASSWORD || "", + timeout: Number(process.env.RR_TIMEOUT || 30000), + dealerNumber: process.env.RR_DEALER_NUMBER || "PPERASV02000000", + storeNumber: process.env.RR_STORE_NUMBER || "05", + branchNumber: process.env.RR_AREA_NUMBER || "03" }; } -/** - * Load RR config overrides from DB (bodyshops_by_pk only). - */ -async function loadBodyshopRRConfig(bodyshopId) { - if (!bodyshopId) return null; - - const client = buildGqlClient(); - const { bodyshops_by_pk: bs } = await client.request(Q_BODYSHOPS_BY_PK, { id: bodyshopId }); - - if (!bs) throw new Error("Bodyshop not found."); - if (!bs.rr_dealerid) throw new Error("Bodyshop is not configured for RR (missing rr_dealerid)."); - - return normalizeConfigRecord(bs); -} - -// ---------------- rr-test logic ---------------- - -function pickActionName(raw) { - if (!raw || typeof raw !== "string") return "ping"; - const x = raw.toLowerCase(); - if (x === "combined" || x === "combinedsearch" || x === "comb") return "combined"; - if (x === "advisors" || x === "advisor" || x === "getadvisors") return "advisors"; - if (x === "parts" || x === "getparts" || x === "part") return "parts"; - if (x === "ping") return "ping"; - return x; -} - -function buildBodyForAction(action, args, cfg) { - switch (action) { - case "ping": - case "advisors": { - const max = toIntOr(1, args.max); - const data = { - DealerCode: cfg.dealerNumber, - DealerNumber: cfg.dealerNumber, - StoreNumber: cfg.storeNumber, - BranchNumber: cfg.branchNumber, - SearchCriteria: { - AdvisorId: args.advisorId, - FirstName: args.first || args.firstname, - LastName: args.last || args.lastname, - Department: args.department, - Status: args.status || "ACTIVE", - IncludeInactive: args.includeInactive ? "true" : undefined, - MaxResults: max +// Utility: get a value from parsed JSON or raw XML (element or attribute) +function findFirstId(parsed, rawXml, keyRegex) { + // search parsed object + if (parsed && typeof parsed === "object") { + const stack = [parsed]; + while (stack.length) { + const cur = stack.pop(); + if (cur && typeof cur === "object") { + for (const [k, v] of Object.entries(cur)) { + if (keyRegex.test(k) && (typeof v === "string" || typeof v === "number")) { + const s = String(v).trim(); + if (s) return s; + } + if (v && typeof v === "object") stack.push(v); } - }; - return { template: "GetAdvisors", data, appArea: {} }; + } } - - case "combined": { - const max = toIntOr(10, args.max); - const data = { - DealerNumber: cfg.dealerNumber, - StoreNumber: cfg.storeNumber, - BranchNumber: cfg.branchNumber, - Customer: { - FirstName: args.first, - LastName: args.last, - PhoneNumber: args.phone, - EmailAddress: args.email - }, - Vehicle: { - VIN: args.vin, - LicensePlate: args.plate - }, - MaxResults: max - }; - return { template: "CombinedSearch", data, appArea: {} }; - } - - case "parts": { - const max = toIntOr(5, args.max); - const data = { - DealerNumber: cfg.dealerNumber, - StoreNumber: cfg.storeNumber, - BranchNumber: cfg.branchNumber, - SearchCriteria: { - PartNumber: args.part, - Description: args.desc, - Make: args.make, - Model: args.model, - Year: args.year, - MaxResults: max - } - }; - return { template: "GetParts", data, appArea: {} }; - } - - default: - throw new Error(`Unsupported action: ${action}`); } + // search raw XML:{{Code}}
- {{Code}}
+