feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint

This commit is contained in:
Dave
2025-10-20 11:23:10 -04:00
parent a4da874a1a
commit 319f3220ed
6 changed files with 721 additions and 425 deletions

View File

@@ -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: <Key>value</Key> or Key="value"
if (rawXml) {
const re1 = new RegExp(`<([A-Za-z0-9:_-]*${keyRegex.source}[A-Za-z0-9:_-]*)>([^<]+)</\\1>`, "i");
const m1 = re1.exec(rawXml);
if (m1 && m1[2]) return m1[2].trim();
const re2 = new RegExp(`${keyRegex.source}="([^"]+)"`, "i");
const m2 = re2.exec(rawXml);
if (m2 && m2[1]) return m2[1].trim();
}
return undefined;
}
async function main() {
const action = pickActionName(args.action || args.a || args._[0]);
const bodyshopId = args.bodyshopId || args.bodyshop || args.b;
// Call wrapper: prints FULL XML
async function callRR(action, dataObj, cfg) {
const respXml = await MakeRRCall({
action,
dealerConfig: cfg,
body: { data: { STAR_NS, ...dataObj }, appArea: appAreaFor(action, cfg) }
});
const parsed = parseRRResponse(respXml);
return { respXml, parsed };
}
const rrAction =
action === "ping"
? "GetAdvisors"
: action === "advisors"
? "GetAdvisors"
: action === "combined"
? "CombinedSearch"
: action === "parts"
? "GetParts"
: action;
// --------- Individual Steps (full XML printed) ---------
// Start with env-based defaults…
const cfg = getBaseRRConfig();
async function stepGetAdvisors(ctx) {
const cfg = ctx.cfg;
const data = {
SearchCriteria: {
Department: FLAG("dept", "B"), // critical to avoid 201
Status: FLAG("status", "ACTIVE"),
MaxResults: Number(FLAG("max", 5)) || 5
}
};
const { respXml, parsed } = await callRR("GetAdvisors", data, cfg);
// …then override with per-bodyshop values if provided.
if (bodyshopId) {
console.log("\n[GetAdvisors] RESPONSE (FULL):\n");
console.log(respXml);
const advId =
findFirstId(parsed.parsed, respXml, /(Advisor(ID|No|Number)?|AdvNo)/i) ||
findFirstId(parsed.parsed, respXml, /(Employee(ID|No|Number)?)/i);
if (advId) ctx.advisorId = advId;
return { ok: true, code: parsed.code, advisorId: advId || null };
}
async function stepCombinedSearch(ctx) {
const cfg = ctx.cfg;
// Provide at least one criterion by default to avoid 201
const defaultLast = FLAG("last", process.env.RR_TEST_LAST || "SMITH");
const defaultPhone = FLAG("phone", process.env.RR_TEST_PHONE || "");
const defaultVin = FLAG("vin", process.env.RR_TEST_VIN || "");
const data = {
MaxResults: Number(FLAG("max", 5)) || 5,
Customer: {},
Vehicle: {}
};
if (defaultLast) data.Customer.LastName = defaultLast;
if (defaultPhone) data.Customer.PhoneNumber = defaultPhone;
if (defaultVin) data.Vehicle.VIN = defaultVin;
const { respXml, parsed } = await callRR("CombinedSearch", data, cfg);
console.log("\n[CombinedSearch] RESPONSE (FULL):\n");
console.log(respXml);
const custId = findFirstId(parsed.parsed, respXml, /(Cust(omer)?(No|Id|ID|Number|Key)|NameRecId)/i);
const vehId = findFirstId(parsed.parsed, respXml, /(Veh(icle)?(No|Id|ID|Number|Key))/i);
const vin = findFirstId(parsed.parsed, respXml, /VIN/i);
if (custId) ctx.customerId = custId;
if (vehId) ctx.vehicleId = vehId;
if (vin) ctx.vin = vin;
return { ok: true, code: parsed.code, customerId: custId || null, vehicleId: vehId || null, vin: vin || null };
}
async function stepInsertCustomer(ctx) {
const cfg = ctx.cfg;
// RR requires IBFlag and LastName for individual customers
const ts = dayjs().format("YYYYMMDD-HHmmss");
const firstName = FLAG("first", process.env.RR_TEST_FIRST || "QA");
const lastName = FLAG("last", process.env.RR_TEST_LAST || "Test");
const email = FLAG("email", process.env.RR_TEST_EMAIL || `qa.${ts}@example.com`);
const phone = FLAG(
"phone",
process.env.RR_TEST_PHONE || `937555${String(Math.floor(Math.random() * 10000)).padStart(4, "0")}`
);
const data = {
IBFlag: "I", // <== REQUIRED (I=Individual, B=Business)
FirstName: firstName, // <== REQUIRED with IBFlag=I
LastName: lastName, // <== REQUIRED with IBFlag=I
Active: "Y",
Phones: [{ Type: "Mobile", CountryCode: "1", Number: phone, Preferred: "Y" }],
Emails: [{ Type: "Personal", Address: email, Preferred: "Y" }],
Addresses: [
{
Type: "Home",
Line1: "123 Test St",
City: "Dayton",
State: "OH",
PostalCode: "45402",
Country: "US",
IsPrimary: "Y"
}
]
};
const { respXml, parsed } = await callRR("InsertCustomer", data, cfg);
console.log("\n[InsertCustomer] RESPONSE (FULL):\n");
console.log(respXml);
// RR often returns the new customer key as DMSRecKey attribute in TransStatus
const custId = findFirstId(parsed.parsed, respXml, /(DMSRecKey|Cust(omer)?(No|Id|ID|Number|Key)|NameRecId)/i);
if (custId) ctx.customerId = custId;
return { ok: !!custId, code: parsed.code, customerId: custId || null };
}
async function stepUpdateCustomer(ctx) {
const cfg = ctx.cfg;
if (!ctx.customerId) return { ok: false, code: "NO_CONTEXT", message: "No customerId" };
const data = {
CustomerId: ctx.customerId,
Emails: [{ Type: "Personal", Address: `qa.${dayjs().format("HHmmss")}@example.com`, Preferred: "Y" }]
};
const { respXml, parsed } = await callRR("UpdateCustomer", data, cfg);
console.log("\n[UpdateCustomer] RESPONSE (FULL):\n");
console.log(respXml);
return { ok: parsed.success, code: parsed.code };
}
async function stepInsertServiceVehicle(ctx) {
const cfg = ctx.cfg;
if (!ctx.customerId) return { ok: false, code: "NO_CONTEXT", message: "No customerId" };
const vin =
FLAG("vin", process.env.RR_TEST_VIN) ||
`1FTFW1E50JFA${String(Math.floor(Math.random() * 1000000)).padStart(6, "0")}`;
const data = {
CustomerId: ctx.customerId,
VIN: vin,
Year: "2018",
Make: "FORD",
Model: "F-150",
Color: "WHITE",
LicensePlate:
FLAG("plate", process.env.RR_TEST_PLATE) || `QA${String(Math.floor(Math.random() * 100000)).padStart(5, "0")}`,
LicenseState: "OH",
Odometer: "123456",
OdometerUnits: "MI"
};
const { respXml, parsed } = await callRR("InsertServiceVehicle", data, cfg);
console.log("\n[InsertServiceVehicle] RESPONSE (FULL):\n");
console.log(respXml);
const vehId = findFirstId(parsed.parsed, respXml, /(Veh(icle)?(No|Id|ID|Number|Key))/i);
if (vehId) ctx.vehicleId = vehId;
ctx.vin = vin;
return { ok: parsed.success || !!vehId, code: parsed.code, vehicleId: vehId || null, vin };
}
async function stepCreateRepairOrder(ctx) {
const cfg = ctx.cfg;
if (!ctx.customerId) return { ok: false, code: "NO_CONTEXT", message: "No customerId" };
const roNumber = `BSM-${dayjs().format("MMDD-HHmmss")}-${String(Math.floor(Math.random() * 1000)).padStart(3, "0")}`;
const data = {
RepairOrderNumber: roNumber,
Department: "B",
ROType: "INS",
Status: "OPEN",
IsBodyShop: "Y",
ServiceAdvisorId: ctx.advisorId || FLAG("advisor", process.env.RR_TEST_ADVISOR) || undefined,
Customer: {
CustomerId: ctx.customerId,
FirstName: FLAG("first", process.env.RR_TEST_FIRST || "QA"),
LastName: FLAG("last", process.env.RR_TEST_LAST || "Test"),
PhoneNumber: FLAG("phone", process.env.RR_TEST_PHONE || "9375550000"),
EmailAddress: FLAG("email", process.env.RR_TEST_EMAIL || "qa@example.com")
},
ServiceVehicle: {
VIN: ctx.vin || FLAG("vin", process.env.RR_TEST_VIN) || undefined,
Odometer: "4321"
},
Totals: {
Currency: "USD",
LaborTotal: "0",
PartsTotal: "0",
MiscTotal: "0",
DiscountTotal: "0",
TaxTotal: "0",
GrandTotal: "0"
}
};
const respXml = await MakeRRCall({
action: "CreateRepairOrder",
dealerConfig: cfg,
body: { data: { STAR_NS, ...data }, appArea: appAreaFor("CreateRepairOrder", cfg) }
});
const parsed = parseRRResponse(respXml);
console.log("\n[CreateRepairOrder] RESPONSE (FULL):\n");
console.log(respXml);
const roNo = findFirstId(parsed.parsed, respXml, /(DMSRoNo|RO?No|OutsdRoNo|RepairOrder(Id|Number))/i) || roNumber;
ctx.roNumber = roNo;
return { ok: parsed.success, code: parsed.code, roNumber: roNo };
}
async function stepUpdateRepairOrder(ctx) {
const cfg = ctx.cfg;
if (!ctx.roNumber) return { ok: false, code: "NO_CONTEXT", message: "No roNumber" };
const data = {
RepairOrderNumber: ctx.roNumber,
Department: "B",
Status: "OPEN",
Totals: { LaborTotal: "0", PartsTotal: "0", MiscTotal: "0", TaxTotal: "0", GrandTotal: "0" }
};
const { respXml, parsed } = await callRR("UpdateRepairOrder", data, cfg);
console.log("\n[UpdateRepairOrder] RESPONSE (FULL):\n");
console.log(respXml);
return { ok: parsed.success, code: parsed.code };
}
async function stepGetParts(ctx) {
const cfg = ctx.cfg;
const data = {
MaxResults: Number(FLAG("max", 5)) || 5,
SearchMode: "Description",
Description: FLAG("partdesc", process.env.RR_TEST_PARTDESC || "clip")
};
const { respXml, parsed } = await callRR("GetParts", data, cfg);
console.log("\n[GetParts] RESPONSE (FULL):\n");
console.log(respXml);
return { ok: parsed.success, code: parsed.code };
}
// --------- Runner ---------
async function runAll() {
await verifyTemplates();
const cfg = cfgFromEnv();
const ctx = { cfg };
const results = [];
async function runStep(name, fn) {
const t0 = Date.now();
try {
const overrides = await loadBodyshopRRConfig(bodyshopId);
if (overrides?.dealerNumber) cfg.dealerNumber = overrides.dealerNumber;
if (overrides?.storeNumber) cfg.storeNumber = overrides.storeNumber;
if (overrides?.branchNumber) cfg.branchNumber = overrides.branchNumber;
console.log(" RR config loaded from DB via: bodyshops_by_pk");
} catch (e) {
console.error("❌ Failed to load per-bodyshop RR config:", e.message);
process.exit(1);
const res = await fn(ctx);
results.push({ step: name, ok: !!res.ok, code: res.code || "OK", ms: Date.now() - t0 });
console.log(`\n${name} done in ${Date.now() - t0}ms`);
return res;
} catch (err) {
results.push({ step: name, ok: false, code: err.code || "ERR", ms: Date.now() - t0, error: err.message });
console.error(`\n${name} failed: ${err.message}`);
throw err;
}
}
const body = buildBodyForAction(action, args, cfg);
const templateName = body.template || rrAction;
// Sequence (dependencies respected)
await runStep("GetAdvisors", stepGetAdvisors);
await runStep("CombinedSearch", stepCombinedSearch);
try {
await renderXmlTemplate(templateName, body.data);
console.log("✅ Templates verified.");
} catch (e) {
console.error("❌ Template verification failed:", e.message);
process.exit(1);
}
if (!ctx.customerId) await runStep("InsertCustomer", stepInsertCustomer);
if (!ctx.vehicleId) await runStep("InsertServiceVehicle", stepInsertServiceVehicle);
if (args.dry) {
const business = await renderXmlTemplate(templateName, body.data);
const envelope = await buildStarEnvelope(business, cfg, body.appArea);
console.log("\n--- FULL SOAP ENVELOPE ---\n");
console.log(envelope);
console.log("\n(dry run) 🚫 Skipping network call.");
await runStep("CreateRepairOrder", stepCreateRepairOrder);
await runStep("UpdateRepairOrder", stepUpdateRepairOrder);
await runStep("GetParts", stepGetParts);
// Summary
console.log("\n=== SUMMARY ===");
console.table(results.map((r) => ({ step: r.step, ok: r.ok ? "✅" : "❌", code: r.code, ms: r.ms })));
const failed = results.some((r) => !r.ok);
console.log(`Total: ${results.reduce((a, r) => a + r.ms, 0)}ms | Status: ${failed ? "FAIL" : "PASS"}`);
if (failed) process.exitCode = 1;
}
// Entrypoint
(async () => {
if (FLAG("ping", false)) {
await verifyTemplates();
const cfg = cfgFromEnv();
console.log("\n▶ Calling Rome action: GetAdvisors");
const r = await stepGetAdvisors({ cfg });
if (r.ok) console.log("\n✅ RR call completed.\n");
return;
}
try {
console.log(`\n▶ Calling Rome action: ${rrAction}`);
const xml = await MakeRRCall({ action: rrAction, body, dealerConfig: cfg });
console.log("\n✅ RR call succeeded.\n");
console.log(xml);
} catch (err) {
console.dir(err);
console.error("[RR] rr-test failed", { message: err.message, stack: err.stack });
process.exit(1);
if (FLAG("all", true)) {
await runAll();
return;
}
}
main().catch(() => process.exit(1));
// Individual flags (optional)
await verifyTemplates();
const cfg = cfgFromEnv();
const ctx = { cfg };
const want = [];
if (FLAG("get-advisors", false)) want.push(stepGetAdvisors);
if (FLAG("combined", false)) want.push(stepCombinedSearch);
if (FLAG("insert-customer", false)) want.push(stepInsertCustomer);
if (FLAG("update-customer", false)) want.push(stepUpdateCustomer);
if (FLAG("insert-vehicle", false)) want.push(stepInsertServiceVehicle);
if (FLAG("create-ro", false)) want.push(stepCreateRepairOrder);
if (FLAG("update-ro", false)) want.push(stepUpdateRepairOrder);
if (FLAG("get-parts", false)) want.push(stepGetParts);
for (const fn of want) {
const res = await fn(ctx).catch((e) => ({ ok: false, code: "ERR", error: e.message }));
console.log(res);
}
})();