feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -32,6 +32,22 @@ function getDSB(cfg) {
|
||||
return { dealerNumber, storeNumber, branchNumber };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an address-like object to the template's <Address> 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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{{#Status}}<Status>{{Status}}</Status>{{/Status}}
|
||||
{{#IsBodyShop}}<IsBodyShop>{{IsBodyShop}}</IsBodyShop>{{/IsBodyShop}}
|
||||
{{#DRPFlag}}<DRPFlag>{{DRPFlag}}</DRPFlag>{{/DRPFlag}}
|
||||
|
||||
<Customer>
|
||||
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
|
||||
@@ -30,6 +31,7 @@
|
||||
</Address>
|
||||
{{/Address}}
|
||||
</Customer>
|
||||
|
||||
<ServiceVehicle>
|
||||
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
|
||||
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||
@@ -40,14 +42,13 @@
|
||||
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||
</ServiceVehicle>
|
||||
|
||||
{{#JobLines}}
|
||||
<JobLine>
|
||||
<Sequence>{{Sequence}}</Sequence>
|
||||
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
|
||||
{{#LineType}}<LineType>
|
||||
{{LineType}}</LineType>{{/LineType}
|
||||
{{#Category}}<Category>
|
||||
{{Category}}</Category>{{/Category}}
|
||||
{{#LineType}}<LineType>{{LineType}}</LineType>{{/LineType}}
|
||||
{{#Category}}<Category>{{Category}}</Category>{{/Category}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||
@@ -61,6 +62,7 @@
|
||||
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
|
||||
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||
|
||||
{{#Taxes}}
|
||||
<Taxes>
|
||||
{{#Items}}
|
||||
@@ -74,6 +76,7 @@
|
||||
{{/Taxes}}
|
||||
</JobLine>
|
||||
{{/JobLines}}
|
||||
|
||||
{{#Totals}}
|
||||
<Totals>
|
||||
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
|
||||
@@ -85,6 +88,7 @@
|
||||
<GrandTotal>{{GrandTotal}}</GrandTotal>
|
||||
</Totals>
|
||||
{{/Totals}}
|
||||
|
||||
{{#Payments}}
|
||||
<Payments>
|
||||
{{#Items}}
|
||||
@@ -99,6 +103,7 @@
|
||||
{{/Items}}
|
||||
</Payments>
|
||||
{{/Payments}}
|
||||
|
||||
{{#Insurance}}
|
||||
<Insurance>
|
||||
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||
@@ -107,6 +112,7 @@
|
||||
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
|
||||
</Insurance>
|
||||
{{/Insurance}}
|
||||
|
||||
{{#Notes}}
|
||||
<Notes>
|
||||
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
<rey_RomeUpdateBSMRepairOrderReq xmlns="{{STAR_NS}}" revision="1.0">
|
||||
<BSMRepairOrderChgReq>
|
||||
<BSMRepairOrderReq>
|
||||
<RepairOrder>
|
||||
{{#RepairOrderId}}<RepairOrderId>{{RepairOrderId}}</RepairOrderId>{{/RepairOrderId}}
|
||||
{{#RepairOrderNumber}}<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>{{/RepairOrderNumber}}
|
||||
{{#Status}}<Status>
|
||||
{{Status}}</Status>{{/Status}}
|
||||
{{#ROType}}<ROType>
|
||||
{{ROType}}</ROType>{{/ROType}}
|
||||
<RepairOrderNumber>{{RepairOrderNumber}}</RepairOrderNumber>
|
||||
{{#DmsRepairOrderId}}<DmsRepairOrderId>{{DmsRepairOrderId}}</DmsRepairOrderId>{{/DmsRepairOrderId}}
|
||||
{{#OpenDate}}<OpenDate>{{OpenDate}}</OpenDate>{{/OpenDate}}
|
||||
{{#PromisedDate}}<PromisedDate>{{PromisedDate}}</PromisedDate>{{/PromisedDate}}
|
||||
{{#CloseDate}}<CloseDate>{{CloseDate}}</CloseDate>{{/CloseDate}}
|
||||
{{#ServiceAdvisorId}}<ServiceAdvisorId>{{ServiceAdvisorId}}</ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||
{{#TechnicianId}}<TechnicianId>{{TechnicianId}}</TechnicianId>{{/TechnicianId}}
|
||||
{{#LocationCode}}<LocationCode>{{LocationCode}}</LocationCode>{{/LocationCode}}
|
||||
{{#Department}}<Department>{{Department}}</Department>{{/Department}}
|
||||
{{#PurchaseOrder}}<PurchaseOrder>{{PurchaseOrder}}</PurchaseOrder>{{/PurchaseOrder}}
|
||||
{{#Customer}}
|
||||
{{#ProfitCenter}}<ProfitCenter>{{ProfitCenter}}</ProfitCenter>{{/ProfitCenter}}
|
||||
{{#ROType}}<ROType>{{ROType}}</ROType>{{/ROType}}
|
||||
{{#Status}}<Status>{{Status}}</Status>{{/Status}}
|
||||
{{#IsBodyShop}}<IsBodyShop>{{IsBodyShop}}</IsBodyShop>{{/IsBodyShop}}
|
||||
{{#DRPFlag}}<DRPFlag>{{DRPFlag}}</DRPFlag>{{/DRPFlag}}
|
||||
|
||||
<Customer>
|
||||
{{#CustomerId}}<CustomerId>{{CustomerId}}</CustomerId>{{/CustomerId}}
|
||||
{{#CustomerName}}<CustomerName>{{CustomerName}}</CustomerName>{{/CustomerName}}
|
||||
{{#PhoneNumber}}<PhoneNumber>{{PhoneNumber}}</PhoneNumber>{{/PhoneNumber}}
|
||||
{{#EmailAddress}}<EmailAddress>{{EmailAddress}}</EmailAddress>{{/EmailAddress}}
|
||||
{{#Address}}
|
||||
<Address>
|
||||
{{#Line1}}<Line1>{{Line1}}</Line1>{{/Line1}}
|
||||
{{#Line2}}<Line2>{{Line2}}</Line2>{{/Line2}}
|
||||
{{#City}}<City>{{City}}</City>{{/City}}
|
||||
{{#State}}<State>{{State}}</State>{{/State}}
|
||||
{{#PostalCode}}<PostalCode>{{PostalCode}}</PostalCode>{{/PostalCode}}
|
||||
{{#Country}}<Country>{{Country}}</Country>{{/Country}}
|
||||
</Address>
|
||||
{{/Address}}
|
||||
</Customer>
|
||||
{{/Customer}}
|
||||
{{#Vehicle}}
|
||||
|
||||
<ServiceVehicle>
|
||||
{{#VehicleId}}<VehicleId>{{VehicleId}}</VehicleId>{{/VehicleId}}
|
||||
{{#VIN}}<VIN>{{VIN}}</VIN>{{/VIN}}
|
||||
@@ -34,93 +42,68 @@
|
||||
{{#Odometer}}<Odometer>{{Odometer}}</Odometer>{{/Odometer}}
|
||||
{{#Color}}<Color>{{Color}}</Color>{{/Color}}
|
||||
</ServiceVehicle>
|
||||
{{/Vehicle}}
|
||||
{{#AddedJobLines}}
|
||||
<AddedJobLines>
|
||||
{{#Items}}
|
||||
<JobLine>
|
||||
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#LineType}}<LineType>
|
||||
{{LineType}}</LineType>{{/LineType}}
|
||||
{{#Category}}<Category>
|
||||
{{Category}}</Category>{{/Category}}
|
||||
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
|
||||
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
|
||||
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||
{{#Taxes}}
|
||||
<Taxes>
|
||||
{{#Items}}
|
||||
<Tax>
|
||||
<Code>{{Code}}</Code>
|
||||
<Amount>{{Amount}}</Amount>
|
||||
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
|
||||
</Tax>
|
||||
{{/Items}}
|
||||
</Taxes>
|
||||
{{/Taxes}}
|
||||
{{#PayType}}<PayType>
|
||||
{{PayType}}</PayType>{{/PayType}}
|
||||
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||
</JobLine>
|
||||
{{/Items}}
|
||||
</AddedJobLines>
|
||||
{{/AddedJobLines}}
|
||||
{{#UpdatedJobLines}}
|
||||
<UpdatedJobLines>
|
||||
{{#Items}}
|
||||
<JobLine>
|
||||
{{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
|
||||
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||
{{#ChangeType}}<ChangeType>
|
||||
{{ChangeType}}</ChangeType>{{/ChangeType}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||
{{#PayType}}<PayType>{{PayType}}</PayType>{{/PayType}}
|
||||
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||
</JobLine>
|
||||
{{/Items}}
|
||||
</UpdatedJobLines>
|
||||
{{/UpdatedJobLines}}
|
||||
{{#RemovedJobLines}}
|
||||
<RemovedJobLines>
|
||||
{{#Items}}
|
||||
<JobLine>
|
||||
{{#LineId}}<LineId>{{LineId}}</LineId>{{/LineId}}
|
||||
{{#Sequence}}<Sequence>{{Sequence}}</Sequence>{{/Sequence}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Reason}}<Reason>{{Reason}}</Reason>{{/Reason}}
|
||||
</JobLine>
|
||||
{{/Items}}
|
||||
</RemovedJobLines>
|
||||
{{/RemovedJobLines}}
|
||||
|
||||
{{#JobLines}}
|
||||
<JobLine>
|
||||
<Sequence>{{Sequence}}</Sequence>
|
||||
{{#ParentSequence}}<ParentSequence>{{ParentSequence}}</ParentSequence>{{/ParentSequence}}
|
||||
{{#LineType}}<LineType>{{LineType}}</LineType>{{/LineType}}
|
||||
{{#Category}}<Category>{{Category}}</Category>{{/Category}}
|
||||
{{#OpCode}}<OpCode>{{OpCode}}</OpCode>{{/OpCode}}
|
||||
{{#Description}}<Description>{{Description}}</Description>{{/Description}}
|
||||
{{#LaborHours}}<LaborHours>{{LaborHours}}</LaborHours>{{/LaborHours}}
|
||||
{{#LaborRate}}<LaborRate>{{LaborRate}}</LaborRate>{{/LaborRate}}
|
||||
{{#PartNumber}}<PartNumber>{{PartNumber}}</PartNumber>{{/PartNumber}}
|
||||
{{#PartDescription}}<PartDescription>{{PartDescription}}</PartDescription>{{/PartDescription}}
|
||||
{{#Quantity}}<Quantity>{{Quantity}}</Quantity>{{/Quantity}}
|
||||
{{#UnitPrice}}<UnitPrice>{{UnitPrice}}</UnitPrice>{{/UnitPrice}}
|
||||
{{#ExtendedPrice}}<ExtendedPrice>{{ExtendedPrice}}</ExtendedPrice>{{/ExtendedPrice}}
|
||||
{{#DiscountAmount}}<DiscountAmount>{{DiscountAmount}}</DiscountAmount>{{/DiscountAmount}}
|
||||
{{#TaxCode}}<TaxCode>{{TaxCode}}</TaxCode>{{/TaxCode}}
|
||||
{{#GLAccount}}<GLAccount>{{GLAccount}}</GLAccount>{{/GLAccount}}
|
||||
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||
|
||||
{{#Taxes}}
|
||||
<Taxes>
|
||||
{{#Items}}
|
||||
<Tax>
|
||||
<Code>{{Code}}</Code>
|
||||
<Amount>{{Amount}}</Amount>
|
||||
{{#Rate}}<Rate>{{Rate}}</Rate>{{/Rate}}
|
||||
</Tax>
|
||||
{{/Items}}
|
||||
</Taxes>
|
||||
{{/Taxes}}
|
||||
</JobLine>
|
||||
{{/JobLines}}
|
||||
|
||||
{{#Totals}}
|
||||
<Totals>
|
||||
{{#Currency}}<Currency>{{Currency}}</Currency>{{/Currency}}
|
||||
{{#LaborTotal}}<LaborTotal>{{LaborTotal}}</LaborTotal>{{/LaborTotal}}
|
||||
{{#PartsTotal}}<PartsTotal>{{PartsTotal}}</PartsTotal>{{/PartsTotal}}
|
||||
{{#MiscTotal}}<MiscTotal>{{MiscTotal}}</MiscTotal>{{/MiscTotal}}
|
||||
{{#DiscountTotal}}<DiscountTotal>{{DiscountTotal}}</DiscountTotal>{{/DiscountTotal}}
|
||||
{{#TaxTotal}}<TaxTotal>{{TaxTotal}}</TaxTotal>{{/TaxTotal}}
|
||||
{{#GrandTotal}}<GrandTotal>{{GrandTotal}}</GrandTotal>{{/GrandTotal}}
|
||||
<GrandTotal>{{GrandTotal}}</GrandTotal>
|
||||
</Totals>
|
||||
{{/Totals}}
|
||||
|
||||
{{#Payments}}
|
||||
<Payments>
|
||||
{{#Items}}
|
||||
<Payment>
|
||||
<PayerType>{{PayerType}}</PayerType>
|
||||
{{#PayerName}}<PayerName>{{PayerName}}</PayerName>{{/PayerName}}
|
||||
<Amount>{{Amount}}</Amount>
|
||||
{{#Method}}<Method>{{Method}}</Method>{{/Method}}
|
||||
{{#Reference}}<Reference>{{Reference}}</Reference>{{/Reference}}
|
||||
{{#ControlNumber}}<ControlNumber>{{ControlNumber}}</ControlNumber>{{/ControlNumber}}
|
||||
</Payment>
|
||||
{{/Items}}
|
||||
</Payments>
|
||||
{{/Payments}}
|
||||
|
||||
{{#Insurance}}
|
||||
<Insurance>
|
||||
{{#CompanyName}}<CompanyName>{{CompanyName}}</CompanyName>{{/CompanyName}}
|
||||
@@ -129,11 +112,12 @@
|
||||
{{#AdjusterPhone}}<AdjusterPhone>{{AdjusterPhone}}</AdjusterPhone>{{/AdjusterPhone}}
|
||||
</Insurance>
|
||||
{{/Insurance}}
|
||||
|
||||
{{#Notes}}
|
||||
<Notes>
|
||||
{{#Items}}<Note>{{.}}</Note>{{/Items}}
|
||||
</Notes>
|
||||
{{/Notes}}
|
||||
</RepairOrder>
|
||||
</BSMRepairOrderChgReq>
|
||||
</BSMRepairOrderReq>
|
||||
</rey_RomeUpdateBSMRepairOrderReq>
|
||||
|
||||
Reference in New Issue
Block a user