feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -14,6 +14,7 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
|
|||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component";
|
import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component";
|
||||||
|
import ShopInfoRRConfigurationComponent from "./shop-info.rr-configuration.component";
|
||||||
|
|
||||||
const SelectorDiv = styled.div`
|
const SelectorDiv = styled.div`
|
||||||
.ant-form-item .ant-select {
|
.ant-form-item .ant-select {
|
||||||
@@ -63,6 +64,8 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
<RbacWrapper action="shop:responsibilitycenter">
|
<RbacWrapper action="shop:responsibilitycenter">
|
||||||
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) && (
|
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) && (
|
||||||
<>
|
<>
|
||||||
|
{bodyshop.rr_dealerid && <ShopInfoRRConfigurationComponent form={form} />}
|
||||||
|
|
||||||
{bodyshop.cdk_dealerid && (
|
{bodyshop.cdk_dealerid && (
|
||||||
<DataLabel label={t("bodyshop.labels.dms.cdk_dealerid")}>{form.getFieldValue("cdk_dealerid")}</DataLabel>
|
<DataLabel label={t("bodyshop.labels.dms.cdk_dealerid")}>{form.getFieldValue("cdk_dealerid")}</DataLabel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Card, Divider, Form, Input, Select, Switch, Tooltip } from "antd";
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reynolds & Reynolds Configuration Section
|
||||||
|
* Stored under bodyshop.rr_configuration (JSONB)
|
||||||
|
*
|
||||||
|
* NOTE:
|
||||||
|
* - Do NOT put credentials/endpoints here. Those live in env secrets.
|
||||||
|
* - These are dealer/location-specific values that the backend reads from rr_configuration.
|
||||||
|
* Keys match server usage: dealer_code, dealer_name, location_id, store_number, branch_number, etc.
|
||||||
|
*/
|
||||||
|
export default function ShopInfoRRConfigurationComponent() {
|
||||||
|
return (
|
||||||
|
<Card title="Reynolds & Reynolds Configuration" variant={false}>
|
||||||
|
{/* Dealer / Location identifiers (dealer-specific, not secrets) */}
|
||||||
|
<Form.Item
|
||||||
|
label="Dealer Code"
|
||||||
|
name={["rr_configuration", "dealer_code"]}
|
||||||
|
tooltip="Your RR Dealer Code / Dealer Number provided in the welcome kit."
|
||||||
|
rules={[{ required: true, message: "Dealer Code is required for RR integration" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="e.g. 12345" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Dealer Name"
|
||||||
|
name={["rr_configuration", "dealer_name"]}
|
||||||
|
tooltip="Optional display name for this dealer as it should appear on outbound requests."
|
||||||
|
>
|
||||||
|
<Input placeholder="e.g. Rome Collision Centre" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Location ID"
|
||||||
|
name={["rr_configuration", "location_id"]}
|
||||||
|
tooltip="If your RR account uses Location/Branch identifiers, enter the Location ID here."
|
||||||
|
>
|
||||||
|
<Input placeholder="e.g. 01" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Store Number"
|
||||||
|
name={["rr_configuration", "store_number"]}
|
||||||
|
tooltip="Optional: RR Store # (from welcome kit)."
|
||||||
|
>
|
||||||
|
<Input placeholder="e.g. 0001" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Branch Number"
|
||||||
|
name={["rr_configuration", "branch_number"]}
|
||||||
|
tooltip="Optional: RR Branch # (from welcome kit)."
|
||||||
|
>
|
||||||
|
<Input placeholder="e.g. 10" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Default Advisor ID"
|
||||||
|
name={["rr_configuration", "default_advisor_id"]}
|
||||||
|
tooltip="Default Service Advisor to assign on RO export (can be overridden per export)."
|
||||||
|
>
|
||||||
|
<Input placeholder="e.g. 007" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Feature flags (safe to store in config) */}
|
||||||
|
<Form.Item
|
||||||
|
label="Enable RR Integration"
|
||||||
|
name={["rr_configuration", "enable_rr_integration"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
tooltip="Master switch to enable/disable RR export for this shop."
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Sandbox Mode"
|
||||||
|
name={["rr_configuration", "sandbox_mode"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
tooltip="Toggles sandbox behavior on the app side. Credentials and URLs remain in env secrets."
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Log XML Requests/Responses"
|
||||||
|
name={["rr_configuration", "log_xml"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
tooltip="When enabled, request/response XML is logged (masked where applicable)."
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Optional UX/format defaults */}
|
||||||
|
<Form.Item
|
||||||
|
label="Default RO Prefix"
|
||||||
|
name={["rr_configuration", "default_ro_prefix"]}
|
||||||
|
tooltip="Optional Repair Order prefix used when generating RO numbers."
|
||||||
|
>
|
||||||
|
<Input placeholder="e.g. RO" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
Timezone{" "}
|
||||||
|
<Tooltip title="Used for date/time fields when building RR payloads.">
|
||||||
|
<span style={{ cursor: "help", color: "var(--ant-color-text-tertiary)" }}>ⓘ</span>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
name={["rr_configuration", "timezone"]}
|
||||||
|
rules={[{ required: true, message: "Timezone is required" }]}
|
||||||
|
>
|
||||||
|
<Select showSearch placeholder="Select timezone">
|
||||||
|
<Option value="America/Toronto">America/Toronto</Option>
|
||||||
|
<Option value="America/New_York">America/New_York</Option>
|
||||||
|
<Option value="America/Chicago">America/Chicago</Option>
|
||||||
|
<Option value="America/Los_Angeles">America/Los_Angeles</Option>
|
||||||
|
<Option value="UTC">UTC</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -124,6 +124,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
md_email_cc
|
md_email_cc
|
||||||
timezone
|
timezone
|
||||||
ss_configuration
|
ss_configuration
|
||||||
|
rr_configuration
|
||||||
md_from_emails
|
md_from_emails
|
||||||
last_name_first
|
last_name_first
|
||||||
md_parts_order_comment
|
md_parts_order_comment
|
||||||
@@ -258,6 +259,7 @@ export const UPDATE_SHOP = gql`
|
|||||||
md_email_cc
|
md_email_cc
|
||||||
timezone
|
timezone
|
||||||
ss_configuration
|
ss_configuration
|
||||||
|
rr_configuration
|
||||||
md_from_emails
|
md_from_emails
|
||||||
last_name_first
|
last_name_first
|
||||||
md_parts_order_comment
|
md_parts_order_comment
|
||||||
|
|||||||
77
package-lock.json
generated
77
package-lock.json
generated
@@ -36,6 +36,7 @@
|
|||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
|
"fast-xml-parser": "^5.3.0",
|
||||||
"firebase-admin": "^13.5.0",
|
"firebase-admin": "^13.5.0",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.6.0",
|
"moment-timezone": "^0.6.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"phone": "^3.1.67",
|
"phone": "^3.1.67",
|
||||||
@@ -1221,18 +1223,6 @@
|
|||||||
"fxparser": "src/cli/cli.js"
|
"fxparser": "src/cli/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aws-sdk/xml-builder/node_modules/strnum": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@aws/lambda-invoke-store": {
|
"node_modules/@aws/lambda-invoke-store": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz",
|
||||||
@@ -1713,6 +1703,38 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@google-cloud/storage/node_modules/fast-xml-parser": {
|
||||||
|
"version": "4.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
|
||||||
|
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"strnum": "^1.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"fxparser": "src/cli/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@google-cloud/storage/node_modules/strnum": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@google-cloud/storage/node_modules/uuid": {
|
"node_modules/@google-cloud/storage/node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
@@ -5895,23 +5917,18 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-xml-parser": {
|
"node_modules/fast-xml-parser": {
|
||||||
"version": "4.4.1",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.0.tgz",
|
||||||
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
|
"integrity": "sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "paypal",
|
|
||||||
"url": "https://paypal.me/naturalintelligence"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"strnum": "^1.0.5"
|
"strnum": "^2.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"fxparser": "src/cli/cli.js"
|
"fxparser": "src/cli/cli.js"
|
||||||
@@ -8158,6 +8175,15 @@
|
|||||||
"node": ">= 10.16.0"
|
"node": ">= 10.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mustache": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mustache": "bin/mustache"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mutexify": {
|
"node_modules/mutexify": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
|
||||||
@@ -10327,17 +10353,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/strnum": {
|
"node_modules/strnum": {
|
||||||
"version": "1.1.2",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
|
||||||
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
|
"integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/stubs": {
|
"node_modules/stubs": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
|
"fast-xml-parser": "^5.3.0",
|
||||||
"firebase-admin": "^13.5.0",
|
"firebase-admin": "^13.5.0",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.6.0",
|
"moment-timezone": "^0.6.0",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"phone": "^3.1.67",
|
"phone": "^3.1.67",
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// RR (Reynolds & Reynolds) HTTP routes
|
* @file rrRoutes.js
|
||||||
// - Mirrors /cdk shape so the UI can switch providers with minimal changes
|
* @description Express Router for Reynolds & Reynolds (Rome) DMS integration.
|
||||||
// - Uses validateFirebaseIdTokenMiddleware + withUserGraphQLClientMiddleware
|
* Provides endpoints for lookup, customer management, repair orders, and full job export.
|
||||||
// - Calls into rr/* modules which wrap MakeRRCall from rr-helpers
|
*/
|
||||||
//
|
|
||||||
// TODO:RR — As you wire the real RR endpoints + schemas, adjust the request
|
|
||||||
// bodies, query params, and response normalization inside rr/* files.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -16,143 +12,197 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl
|
|||||||
|
|
||||||
const { RrCombinedSearch, RrGetAdvisors, RrGetParts } = require("../rr/rr-lookup");
|
const { RrCombinedSearch, RrGetAdvisors, RrGetParts } = require("../rr/rr-lookup");
|
||||||
const { RrCustomerInsert, RrCustomerUpdate } = require("../rr/rr-customer");
|
const { RrCustomerInsert, RrCustomerUpdate } = require("../rr/rr-customer");
|
||||||
// NOTE: correct filename is rr-repair-orders.js (plural)
|
|
||||||
const { CreateRepairOrder, UpdateRepairOrder } = require("../rr/rr-repair-orders");
|
const { CreateRepairOrder, UpdateRepairOrder } = require("../rr/rr-repair-orders");
|
||||||
|
const { ExportJobToRR } = require("../rr/rr-job-export");
|
||||||
|
const RRLogger = require("../rr/rr-logger");
|
||||||
|
|
||||||
// Require auth on all RR routes (keep parity with /cdk)
|
/**
|
||||||
|
* Apply global middlewares:
|
||||||
|
* - Firebase token validation (auth)
|
||||||
|
* - GraphQL client injection (Hasura access)
|
||||||
|
*/
|
||||||
router.use(validateFirebaseIdTokenMiddleware);
|
router.use(validateFirebaseIdTokenMiddleware);
|
||||||
|
router.use(withUserGraphQLClientMiddleware);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// Accounting parity / scaffolding
|
* Health check / diagnostic route
|
||||||
// -----------------------------------------------------------------------------
|
*/
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
res.status(200).json({ provider: "Reynolds & Reynolds (Rome)", status: "OK" });
|
||||||
|
});
|
||||||
|
|
||||||
// Reuse CDK allocations for now; keep the endpoint name identical to /cdk
|
/**
|
||||||
router.post("/calculate-allocations", withUserGraphQLClientMiddleware, async (req, res) => {
|
* Full DMS export for a single job
|
||||||
|
* POST /rr/job/export
|
||||||
|
* Body: { JobData: {...} }
|
||||||
|
*/
|
||||||
|
router.post("/job/export", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
const { JobData } = req.body;
|
||||||
const result = await CalculateAllocations(req, req.body.jobid, true); // verbose=true (like Fortellis flow)
|
RRLogger(req, "info", "RR /job/export initiated", { jobid: JobData?.id });
|
||||||
res.status(200).json({ data: result });
|
|
||||||
} catch (e) {
|
const result = await ExportJobToRR({
|
||||||
req.logger?.log("rr-calc-allocations-route", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
socket: req,
|
||||||
res.status(500).json({ error: e.message });
|
redisHelpers: req.sessionUtils,
|
||||||
|
JobData
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(result.success ? 200 : 500).json(result);
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(req, "error", `RR /job/export failed: ${error.message}`);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Placeholder for a future RR "get vehicles" endpoint to match /cdk/getvehicles
|
/**
|
||||||
router.post("/getvehicles", withUserGraphQLClientMiddleware, async (_req, res) => {
|
* Customer insert
|
||||||
res.status(501).json({ error: "RR getvehicles not implemented yet" });
|
* POST /rr/customer/insert
|
||||||
});
|
*/
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Lookup endpoints
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// GET /rr/lookup/combined?vin=...&lastName=...
|
|
||||||
router.get("/lookup/combined", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const params = Object.entries(req.query); // [["vin","..."], ["lastName","..."]]
|
|
||||||
const data = await RrCombinedSearch({ socket: req, redisHelpers: req.sessionUtils, jobid: "ad-hoc", params });
|
|
||||||
res.status(200).json({ data });
|
|
||||||
} catch (e) {
|
|
||||||
req.logger?.log("rr-lookup-combined", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
|
||||||
res.status(500).json({ error: e.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /rr/advisors?locationId=...
|
|
||||||
router.get("/advisors", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const params = Object.entries(req.query);
|
|
||||||
const data = await RrGetAdvisors({ socket: req, redisHelpers: req.sessionUtils, jobid: "ad-hoc", params });
|
|
||||||
res.status(200).json({ data });
|
|
||||||
} catch (e) {
|
|
||||||
req.logger?.log("rr-get-advisors", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
|
||||||
res.status(500).json({ error: e.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /rr/parts?partNumber=...&make=...
|
|
||||||
router.get("/parts", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const params = Object.entries(req.query);
|
|
||||||
const data = await RrGetParts({ socket: req, redisHelpers: req.sessionUtils, jobid: "ad-hoc", params });
|
|
||||||
res.status(200).json({ data });
|
|
||||||
} catch (e) {
|
|
||||||
req.logger?.log("rr-get-parts", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
|
||||||
res.status(500).json({ error: e.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Customer endpoints
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// POST /rr/customer/insert
|
|
||||||
// Body: { ...JobData-like shape used by rr-mappers }
|
|
||||||
router.post("/customer/insert", async (req, res) => {
|
router.post("/customer/insert", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const data = await RrCustomerInsert({ socket: req, redisHelpers: req.sessionUtils, JobData: req.body });
|
const { JobData } = req.body;
|
||||||
res.status(200).json({ data });
|
const data = await RrCustomerInsert({
|
||||||
} catch (e) {
|
socket: req,
|
||||||
req.logger?.log("rr-customer-insert", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
redisHelpers: req.sessionUtils,
|
||||||
res.status(500).json({ error: e.message });
|
JobData
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(req, "error", `RR /customer/insert failed: ${error.message}`);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /rr/customer/update/:id
|
/**
|
||||||
// Body: { JobData, existingCustomer, patch }
|
* Customer update
|
||||||
|
* PUT /rr/customer/update/:id
|
||||||
|
*/
|
||||||
router.put("/customer/update/:id", async (req, res) => {
|
router.put("/customer/update/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const { JobData, existingCustomer, patch } = req.body;
|
||||||
const data = await RrCustomerUpdate({
|
const data = await RrCustomerUpdate({
|
||||||
socket: req,
|
socket: req,
|
||||||
redisHelpers: req.sessionUtils,
|
redisHelpers: req.sessionUtils,
|
||||||
JobData: req.body?.JobData,
|
JobData,
|
||||||
existingCustomer: req.body?.existingCustomer,
|
existingCustomer,
|
||||||
patch: req.body?.patch
|
patch
|
||||||
});
|
});
|
||||||
res.status(200).json({ data });
|
|
||||||
} catch (e) {
|
res.status(200).json({ success: true, data });
|
||||||
req.logger?.log("rr-customer-update", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
} catch (error) {
|
||||||
res.status(500).json({ error: e.message });
|
RRLogger(req, "error", `RR /customer/update failed: ${error.message}`);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// Repair Order endpoints
|
* Create Repair Order
|
||||||
// -----------------------------------------------------------------------------
|
* POST /rr/repair-order/create
|
||||||
|
*/
|
||||||
// POST /rr/repair-order/create
|
|
||||||
// Body: { JobData, txEnvelope }
|
|
||||||
router.post("/repair-order/create", async (req, res) => {
|
router.post("/repair-order/create", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const { JobData, txEnvelope } = req.body;
|
||||||
const data = await CreateRepairOrder({
|
const data = await CreateRepairOrder({
|
||||||
socket: req,
|
socket: req,
|
||||||
redisHelpers: req.sessionUtils,
|
redisHelpers: req.sessionUtils,
|
||||||
JobData: req.body?.JobData,
|
JobData,
|
||||||
txEnvelope: req.body?.txEnvelope
|
txEnvelope
|
||||||
});
|
});
|
||||||
res.status(200).json({ data });
|
|
||||||
} catch (e) {
|
res.status(200).json({ success: true, data });
|
||||||
req.logger?.log("rr-ro-create", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
} catch (error) {
|
||||||
res.status(500).json({ error: e.message });
|
RRLogger(req, "error", `RR /repair-order/create failed: ${error.message}`);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /rr/repair-order/update/:id
|
/**
|
||||||
// Body: { JobData, txEnvelope }
|
* Update Repair Order
|
||||||
|
* PUT /rr/repair-order/update/:id
|
||||||
|
*/
|
||||||
router.put("/repair-order/update/:id", async (req, res) => {
|
router.put("/repair-order/update/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const { JobData, txEnvelope } = req.body;
|
||||||
const data = await UpdateRepairOrder({
|
const data = await UpdateRepairOrder({
|
||||||
socket: req,
|
socket: req,
|
||||||
redisHelpers: req.sessionUtils,
|
redisHelpers: req.sessionUtils,
|
||||||
JobData: req.body?.JobData,
|
JobData,
|
||||||
txEnvelope: req.body?.txEnvelope
|
txEnvelope
|
||||||
});
|
});
|
||||||
res.status(200).json({ data });
|
|
||||||
} catch (e) {
|
res.status(200).json({ success: true, data });
|
||||||
req.logger?.log("rr-ro-update", "ERROR", "api", "rr", { message: e.message, stack: e.stack });
|
} catch (error) {
|
||||||
res.status(500).json({ error: e.message });
|
RRLogger(req, "error", `RR /repair-order/update failed: ${error.message}`);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined search (customer + service vehicle)
|
||||||
|
* GET /rr/lookup/combined?vin=XXX&lastname=DOE
|
||||||
|
*/
|
||||||
|
router.get("/lookup/combined", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const params = Object.entries(req.query);
|
||||||
|
const data = await RrCombinedSearch({
|
||||||
|
socket: req,
|
||||||
|
redisHelpers: req.sessionUtils,
|
||||||
|
jobid: "ad-hoc",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
res.status(200).json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(req, "error", `RR /lookup/combined failed: ${error.message}`);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Advisors
|
||||||
|
* GET /rr/advisors
|
||||||
|
*/
|
||||||
|
router.get("/advisors", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const params = Object.entries(req.query);
|
||||||
|
const data = await RrGetAdvisors({
|
||||||
|
socket: req,
|
||||||
|
redisHelpers: req.sessionUtils,
|
||||||
|
jobid: "ad-hoc",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
res.status(200).json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(req, "error", `RR /advisors failed: ${error.message}`);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Parts
|
||||||
|
* GET /rr/parts
|
||||||
|
*/
|
||||||
|
router.get("/parts", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const params = Object.entries(req.query);
|
||||||
|
const data = await RrGetParts({
|
||||||
|
socket: req,
|
||||||
|
redisHelpers: req.sessionUtils,
|
||||||
|
jobid: "ad-hoc",
|
||||||
|
params
|
||||||
|
});
|
||||||
|
res.status(200).json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(req, "error", `RR /parts failed: ${error.message}`);
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not implemented placeholder (for future expansion)
|
||||||
|
*/
|
||||||
|
router.post("/calculate-allocations", async (req, res) => {
|
||||||
|
res.status(501).json({ error: "RR calculate-allocations not yet implemented" });
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
79
server/rr/rr-constants.js
Normal file
79
server/rr/rr-constants.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @file rr-constants.js
|
||||||
|
* @description Central constants and configuration for Reynolds & Reynolds (R&R) integration.
|
||||||
|
* Platform-level secrets (API base URL, username, password, ppsysId, dealer/store/branch) are loaded from .env
|
||||||
|
* Dealer-specific values (overrides) come from bodyshop.rr_configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RR_TIMEOUT_MS = 30000; // 30-second SOAP call timeout
|
||||||
|
const RR_NAMESPACE_URI = "http://reynoldsandrey.com/";
|
||||||
|
const RR_DEFAULT_MAX_RESULTS = 25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps internal operation names to Reynolds & Reynolds SOAP actions.
|
||||||
|
* soapAction is sent as the SOAPAction header; URL selection happens in rr-helpers.
|
||||||
|
*/
|
||||||
|
const RR_ACTIONS = {
|
||||||
|
GetAdvisors: { soapAction: "GetAdvisors" },
|
||||||
|
GetParts: { soapAction: "GetParts" },
|
||||||
|
CombinedSearch: { soapAction: "CombinedSearch" },
|
||||||
|
InsertCustomer: { soapAction: "CustomerInsert" },
|
||||||
|
UpdateCustomer: { soapAction: "CustomerUpdate" },
|
||||||
|
InsertServiceVehicle: { soapAction: "ServiceVehicleInsert" },
|
||||||
|
CreateRepairOrder: { soapAction: "RepairOrderInsert" },
|
||||||
|
UpdateRepairOrder: { soapAction: "RepairOrderUpdate" }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default SOAP HTTP headers. SOAPAction is dynamically set per request.
|
||||||
|
*/
|
||||||
|
const RR_SOAP_HEADERS = {
|
||||||
|
"Content-Type": "text/xml; charset=utf-8",
|
||||||
|
SOAPAction: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the rendered XML body inside a SOAP envelope.
|
||||||
|
* @param {string} xmlBody - Inner request XML
|
||||||
|
* @param {string} [headerXml] - Optional header XML (already namespaced)
|
||||||
|
*/
|
||||||
|
const buildSoapEnvelope = (xmlBody, headerXml = "") => `
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="${RR_NAMESPACE_URI}">
|
||||||
|
<soapenv:Header>
|
||||||
|
${headerXml}
|
||||||
|
</soapenv:Header>
|
||||||
|
<soapenv:Body>
|
||||||
|
${xmlBody}
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads base configuration for R&R requests from environment variables.
|
||||||
|
* Dealer-specific overrides come from bodyshop.rr_configuration in the DB.
|
||||||
|
*/
|
||||||
|
const getBaseRRConfig = () => ({
|
||||||
|
// IMPORTANT: RCI Receive endpoint ends with .ashx
|
||||||
|
baseUrl: process.env.RR_API_BASE_URL || "https://b2b-test.reyrey.com/Sync/RCI/Rome/Receive.ashx",
|
||||||
|
username: process.env.RR_API_USER || "",
|
||||||
|
password: process.env.RR_API_PASS || "",
|
||||||
|
ppsysId: process.env.RR_PPSYS_ID || "",
|
||||||
|
|
||||||
|
// Welcome Kit often provides these (used in SOAP header)
|
||||||
|
dealerNumber: process.env.RR_DEALER_NUMBER || "",
|
||||||
|
storeNumber: process.env.RR_STORE_NUMBER || "",
|
||||||
|
branchNumber: process.env.RR_BRANCH_NUMBER || "",
|
||||||
|
|
||||||
|
dealerDefault: process.env.RR_DEFAULT_DEALER || "ROME",
|
||||||
|
timeout: RR_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
RR_TIMEOUT_MS,
|
||||||
|
RR_NAMESPACE_URI,
|
||||||
|
RR_DEFAULT_MAX_RESULTS,
|
||||||
|
RR_ACTIONS,
|
||||||
|
RR_SOAP_HEADERS,
|
||||||
|
buildSoapEnvelope,
|
||||||
|
getBaseRRConfig
|
||||||
|
};
|
||||||
@@ -1,66 +1,137 @@
|
|||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// RR Customer endpoints (create/update) wired through MakeRRCall.
|
* @file rr-customer.js
|
||||||
// Shapes are mapped via rr-mappers.js and validated via rr-error.js.
|
* @description Reynolds & Reynolds (Rome) Customer Insert/Update integration.
|
||||||
//
|
* Builds request payloads using rr-mappers and executes via rr-helpers.
|
||||||
// What’s still missing (complete when you wire to the PDFs):
|
* All dealer-specific data (DealerNumber, LocationId, etc.) is read from the DB (bodyshop.rr_configuration).
|
||||||
// - Final request envelopes & field names in rr-mappers.js
|
*/
|
||||||
// - Definitive success/error envelope checks in rr-error.js
|
|
||||||
// - Any RR-specific headers (dealer/tenant/site) once known
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
||||||
const { assertRrOk } = require("./rr-error");
|
const { assertRrOk } = require("./rr-error");
|
||||||
const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
|
const { mapCustomerInsert, mapCustomerUpdate } = require("./rr-mappers");
|
||||||
|
const RRLogger = require("./rr-logger");
|
||||||
|
const { client } = require("../graphql-client/graphql-client");
|
||||||
|
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a customer in RR.
|
* Fetch rr_configuration for the current bodyshop directly from DB.
|
||||||
|
* This ensures we always have the latest Dealer/Location mapping.
|
||||||
|
*/
|
||||||
|
async function getDealerConfigFromDB(bodyshopId, logger) {
|
||||||
|
try {
|
||||||
|
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||||
|
const config = result?.bodyshops_by_pk?.rr_configuration || null;
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config);
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, {
|
||||||
|
bodyshopId,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CUSTOMER INSERT (Rome Customer Insert Specification 1.2)
|
||||||
|
* Creates a new customer record in the DMS.
|
||||||
*
|
*
|
||||||
* @param {Object} deps
|
* @param {object} options
|
||||||
* @param {Socket|ExpressRequest} deps.socket
|
* @param {object} options.socket - socket.io connection or express req
|
||||||
* @param {Object} deps.redisHelpers - redisHelpers API (not used here directly)
|
* @param {object} options.redisHelpers
|
||||||
* @param {Object} deps.JobData - Rome Job data used to build the payload
|
* @param {object} options.JobData - normalized job record
|
||||||
* @returns {Promise<any>} RR response (envelope TBD)
|
|
||||||
*/
|
*/
|
||||||
async function RrCustomerInsert({ socket, redisHelpers, JobData }) {
|
async function RrCustomerInsert({ socket, redisHelpers, JobData }) {
|
||||||
// Map JobData -> RR "Customer Insert" request body
|
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||||
const body = mapCustomerInsert(JobData);
|
const logger = socket?.logger || console;
|
||||||
|
|
||||||
const data = await MakeRRCall({
|
try {
|
||||||
...RRActions.CreateCustomer, // POST /customer/v1/
|
RRLogger(socket, "info", "RR Customer Insert started", { jobid: JobData?.id, bodyshopId });
|
||||||
body,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData?.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: assertRrOk should be updated once RR’s success envelope is finalized
|
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
||||||
return assertRrOk(data, { apiName: "RR Create Customer" });
|
|
||||||
|
// Build Mustache variables for the InsertCustomer.xml template
|
||||||
|
const vars = mapCustomerInsert(JobData, dealerConfig);
|
||||||
|
|
||||||
|
const data = await MakeRRCall({
|
||||||
|
action: RRActions.CreateCustomer, // resolves to SOAPAction + URL
|
||||||
|
body: { template: "InsertCustomer", data: vars }, // render server/rr/xml-templates/InsertCustomer.xml
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid: JobData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = assertRrOk(data, { apiName: "RR Create Customer" });
|
||||||
|
RRLogger(socket, "debug", "RR Customer Insert success", {
|
||||||
|
jobid: JobData?.id,
|
||||||
|
dealer: dealerConfig?.dealerCode || dealerConfig?.dealer_code
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR Customer Insert failed: ${error.message}`, { jobid: JobData?.id });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing customer in RR.
|
* CUSTOMER UPDATE (Rome Customer Update Specification 1.2)
|
||||||
|
* Updates an existing RR customer record.
|
||||||
*
|
*
|
||||||
* @param {Object} deps
|
* @param {object} options
|
||||||
* @param {Socket|ExpressRequest} deps.socket
|
* @param {object} options.socket
|
||||||
* @param {Object} deps.redisHelpers
|
* @param {object} options.redisHelpers
|
||||||
* @param {Object} deps.JobData - context only (job id for correlation)
|
* @param {object} options.JobData
|
||||||
* @param {Object} deps.existingCustomer - Current RR customer record
|
* @param {object} options.existingCustomer - current RR customer record (from Combined Search)
|
||||||
* @param {Object} deps.patch - Minimal delta from UI to apply onto existingCustomer
|
* @param {object} options.patch - updated fields from frontend
|
||||||
* @returns {Promise<any>} RR response
|
|
||||||
*/
|
*/
|
||||||
async function RrCustomerUpdate({ socket, redisHelpers, JobData, existingCustomer, patch }) {
|
async function RrCustomerUpdate({ socket, redisHelpers, JobData, existingCustomer, patch }) {
|
||||||
// Build a merged/normalized payload for RR Update
|
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||||
const body = mapCustomerUpdate(existingCustomer, patch);
|
const logger = socket?.logger || console;
|
||||||
|
|
||||||
const data = await MakeRRCall({
|
try {
|
||||||
...RRActions.UpdateCustomer, // PUT /customer/v1/ (append id inside body/path per final spec)
|
RRLogger(socket, "info", "RR Customer Update started", {
|
||||||
body,
|
jobid: JobData?.id,
|
||||||
redisHelpers,
|
bodyshopId,
|
||||||
socket,
|
existingCustomerId: existingCustomer?.CustomerId
|
||||||
jobid: JobData?.id
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return assertRrOk(data, { apiName: "RR Update Customer" });
|
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
||||||
|
|
||||||
|
// Build Mustache variables for the UpdateCustomer.xml template
|
||||||
|
const vars = mapCustomerUpdate(existingCustomer, patch, dealerConfig);
|
||||||
|
|
||||||
|
const data = await MakeRRCall({
|
||||||
|
action: RRActions.UpdateCustomer, // resolves to SOAPAction + URL
|
||||||
|
body: { template: "UpdateCustomer", data: vars }, // render server/rr/xml-templates/UpdateCustomer.xml
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid: JobData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = assertRrOk(data, { apiName: "RR Update Customer" });
|
||||||
|
RRLogger(socket, "debug", "RR Customer Update success", {
|
||||||
|
jobid: JobData?.id,
|
||||||
|
customerId: existingCustomer?.CustomerId
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR Customer Update failed: ${error.message}`, {
|
||||||
|
jobid: JobData?.id,
|
||||||
|
customerId: existingCustomer?.CustomerId
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { RrCustomerInsert, RrCustomerUpdate };
|
module.exports = {
|
||||||
|
RrCustomerInsert,
|
||||||
|
RrCustomerUpdate,
|
||||||
|
getDealerConfigFromDB
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,67 +1,103 @@
|
|||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// Error handling utilities for Reynolds & Reynolds (RR) API calls.
|
* @file rr-error.js
|
||||||
// This mirrors Fortellis/CDK error helpers so the call pipeline stays uniform.
|
* @description Centralized error class and assertion logic for Reynolds & Reynolds API calls.
|
||||||
//
|
* Provides consistent handling across all RR modules (customer, repair order, lookups, etc.)
|
||||||
// TODO:RR — Replace the heuristics in assertRrOk with the *actual* envelope and
|
*/
|
||||||
// status semantics from the Rome RR specs. Examples in the PDFs may show:
|
|
||||||
// - <Status code="0" severity="INFO">Success</Status>
|
|
||||||
// - <Status code="123" severity="ERROR">Some message</Status>
|
|
||||||
// - or a SuccessFlag/ReturnCode element in the JSON/XML response.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Error type for RR API responses
|
||||||
|
*/
|
||||||
class RrApiError extends Error {
|
class RrApiError extends Error {
|
||||||
/**
|
|
||||||
* @param {string} message - Human-readable message
|
|
||||||
* @param {object} opts
|
|
||||||
* @param {string} [opts.reqId] - Internal request identifier
|
|
||||||
* @param {string} [opts.url] - Target URL of the API call
|
|
||||||
* @param {string} [opts.apiName] - Which API was invoked (for context)
|
|
||||||
* @param {object} [opts.errorData] - Raw error payload from RR
|
|
||||||
* @param {number} [opts.status] - HTTP status code
|
|
||||||
* @param {string} [opts.statusText] - HTTP status text
|
|
||||||
*/
|
|
||||||
constructor(message, { reqId, url, apiName, errorData, status, statusText } = {}) {
|
constructor(message, { reqId, url, apiName, errorData, status, statusText } = {}) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "RrApiError";
|
this.name = "RrApiError";
|
||||||
this.reqId = reqId;
|
this.reqId = reqId || null;
|
||||||
this.url = url;
|
this.url = url || null;
|
||||||
this.apiName = apiName;
|
this.apiName = apiName || null;
|
||||||
this.errorData = errorData;
|
this.errorData = errorData || null;
|
||||||
this.status = status;
|
this.status = status || null;
|
||||||
this.statusText = statusText;
|
this.statusText = statusText || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that an RR API response is considered "OK".
|
* Assert that a Reynolds & Reynolds response is successful.
|
||||||
* Throws RrApiError otherwise.
|
|
||||||
*
|
*
|
||||||
* @param {*} data - Parsed response object from MakeRRCall
|
* Expected success structure (based on Rome RR specs):
|
||||||
* @param {object} opts
|
* {
|
||||||
* @param {string} opts.apiName - Which API we're checking (for error messages)
|
* "SuccessFlag": true,
|
||||||
* @param {boolean} [opts.allowEmpty=false] - If true, allow null/empty results
|
* "ErrorCode": "0",
|
||||||
* @returns {*} - The same data if valid
|
* "ErrorMessage": "",
|
||||||
|
* "Data": { ... }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Or if SOAP/XML-based:
|
||||||
|
* {
|
||||||
|
* "Envelope": {
|
||||||
|
* "Body": {
|
||||||
|
* "Response": {
|
||||||
|
* "SuccessFlag": true,
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* This helper unwraps and normalizes the response to detect any error cases.
|
||||||
*/
|
*/
|
||||||
function assertRrOk(data, { apiName, allowEmpty = false } = {}) {
|
function assertRrOk(data, { apiName = "RR API Call", allowEmpty = false } = {}) {
|
||||||
// TODO:RR — Update logic to exactly match RR's success envelope.
|
if (!data && !allowEmpty) {
|
||||||
// Possible patterns to confirm from PDFs:
|
throw new RrApiError(`${apiName} returned no data`, { apiName });
|
||||||
// - data.Status?.code === "0"
|
|
||||||
// - data.Return?.successFlag === true
|
|
||||||
// - data.Errors is missing or empty
|
|
||||||
//
|
|
||||||
// For now, we use a simple heuristic fallback.
|
|
||||||
|
|
||||||
const hasErrors =
|
|
||||||
data == null ||
|
|
||||||
data.error ||
|
|
||||||
(Array.isArray(data.errors) && data.errors.length > 0) ||
|
|
||||||
(data.Status && data.Status.severity === "ERROR");
|
|
||||||
|
|
||||||
if (!allowEmpty && hasErrors) {
|
|
||||||
throw new RrApiError(`${apiName} returned an error`, { errorData: data, apiName });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
// Normalize envelope
|
||||||
|
const response =
|
||||||
|
data?.Envelope?.Body?.Response ||
|
||||||
|
data?.Envelope?.Body?.[Object.keys(data.Envelope?.Body || {})[0]] ||
|
||||||
|
data?.Response ||
|
||||||
|
data;
|
||||||
|
|
||||||
|
// Handle array of errors or error objects
|
||||||
|
const errorBlock = response?.Errors || response?.Error || response?.Fault || null;
|
||||||
|
|
||||||
|
// Basic success conditions per RR documentation
|
||||||
|
const success =
|
||||||
|
response?.SuccessFlag === true ||
|
||||||
|
response?.ErrorCode === "0" ||
|
||||||
|
response?.ResultCode === "0" ||
|
||||||
|
(Array.isArray(errorBlock) && errorBlock.length === 0);
|
||||||
|
|
||||||
|
// If success, return normalized response
|
||||||
|
if (success || allowEmpty) {
|
||||||
|
return response?.Data || response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct contextual error info
|
||||||
|
const errorMessage = response?.ErrorMessage || response?.FaultString || response?.Message || "Unknown RR API error";
|
||||||
|
|
||||||
|
throw new RrApiError(`${apiName} failed: ${errorMessage}`, {
|
||||||
|
apiName,
|
||||||
|
errorData: response,
|
||||||
|
status: response?.ErrorCode || response?.ResultCode
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { RrApiError, assertRrOk };
|
/**
|
||||||
|
* Safely unwrap nested RR API responses for consistency across handlers.
|
||||||
|
*/
|
||||||
|
function extractRrResponseData(data) {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
data?.Envelope?.Body?.Response?.Data ||
|
||||||
|
data?.Envelope?.Body?.[Object.keys(data.Envelope?.Body || {})[0]]?.Data ||
|
||||||
|
data?.Data ||
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
RrApiError,
|
||||||
|
assertRrOk,
|
||||||
|
extractRrResponseData
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,488 +1,257 @@
|
|||||||
/**
|
/**
|
||||||
* RR (Reynolds & Reynolds) helper module
|
* @file rr-helpers.js
|
||||||
* -----------------------------------------------------------------------------
|
* @description Core helper functions for Reynolds & Reynolds integration.
|
||||||
* Responsibilities
|
* Handles XML rendering, SOAP communication, and configuration merging.
|
||||||
* - Load env (.env.{NODE_ENV})
|
|
||||||
* - Provide token retrieval with simple Redis-backed caching
|
|
||||||
* - Normalized HTTP caller (MakeRRCall) with request-id + idempotency key
|
|
||||||
* - URL constructor w/ path + query params
|
|
||||||
* - Optional delayed/batch polling stub (DelayedCallback)
|
|
||||||
* - Central action registry (RRActions) with prod/uat base URLs (PLACEHOLDERS)
|
|
||||||
* - Common cache enums + TTL + transaction-type helper (parity with Fortellis)
|
|
||||||
*
|
|
||||||
* What’s missing / TODOs to make this “real” (per RR/Rome PDFs you provided):
|
|
||||||
* - Implement the actual RR auth/token flow inside getRRToken()
|
|
||||||
* - Replace all RRActions URLs with the final endpoints from the RR spec
|
|
||||||
* - Confirm final header names (e.g., X-Request-Id, Idempotency-Key)
|
|
||||||
* - If RR uses async “batch/status/result”, adapt DelayedCallback() to spec
|
|
||||||
* - Confirm success/error envelope and centralize in rr-error.js
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const fs = require("fs/promises");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
require("dotenv").config({
|
const mustache = require("mustache");
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
const axios = require("axios");
|
||||||
});
|
const { v4: uuidv4 } = require("uuid");
|
||||||
|
const { RR_SOAP_HEADERS, RR_ACTIONS, getBaseRRConfig } = require("./rr-constants");
|
||||||
|
const RRLogger = require("./rr-logger");
|
||||||
|
const { client } = require("../graphql-client/graphql-client");
|
||||||
|
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||||
|
|
||||||
const uuid = require("uuid").v4;
|
/* ------------------------------------------------------------------------------------------------
|
||||||
const AxiosLib = require("axios").default;
|
* Configuration
|
||||||
const axios = AxiosLib.create();
|
* ----------------------------------------------------------------------------------------------*/
|
||||||
const axiosCurlirize = require("axios-curlirize").default;
|
|
||||||
|
|
||||||
const logger = require("../utils/logger");
|
/**
|
||||||
const { RrApiError } = require("./rr-error");
|
* Loads the rr_configuration JSON for a given bodyshop directly from the database.
|
||||||
|
* Dealer-level settings only. Platform/secret defaults come from getBaseRRConfig().
|
||||||
// Emit curl equivalents for dev troubleshooting (safe to disable in prod)
|
* @param {string} bodyshopId
|
||||||
axiosCurlirize(axios, (result /*, err */) => {
|
* @returns {Promise<object>} rr_configuration
|
||||||
|
*/
|
||||||
|
async function getDealerConfig(bodyshopId) {
|
||||||
try {
|
try {
|
||||||
const { command } = result;
|
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||||
// Pipe to your centralized logger if preferred:
|
const cfg = result?.bodyshops_by_pk?.rr_configuration || {};
|
||||||
// logger.log("rr-axios-curl", "DEBUG", "api", null, { command });
|
return cfg;
|
||||||
if (process.env.NODE_ENV !== "production") {
|
} catch (err) {
|
||||||
console.log("*** rr axios (curl):", command);
|
console.error(`[RR] Failed to load rr_configuration for bodyshop ${bodyshopId}:`, err.message);
|
||||||
}
|
return {};
|
||||||
} catch {
|
|
||||||
// Best-effort only
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transaction key namespace (mirrors Fortellis' getTransactionType)
|
|
||||||
* Used to partition per-job Redis session hashes.
|
|
||||||
*/
|
|
||||||
const getTransactionType = (jobid) => `rr:${jobid}`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default per-transaction TTL for RR data cached in Redis (seconds).
|
|
||||||
* Keep parity with the Fortellis helper to avoid drift.
|
|
||||||
*/
|
|
||||||
const defaultRRTTL = 60 * 60; // 1 hour
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Namespaced keys stored under each transaction hash (parity with Fortellis)
|
|
||||||
* These are referenced across rr-job-export.js (and friends).
|
|
||||||
*/
|
|
||||||
const RRCacheEnums = {
|
|
||||||
txEnvelope: "txEnvelope",
|
|
||||||
DMSBatchTxn: "DMSBatchTxn",
|
|
||||||
SubscriptionMeta: "SubscriptionMeta", // kept for parity; not used yet for RR
|
|
||||||
DepartmentId: "DepartmentId", // kept for parity; not used yet for RR
|
|
||||||
JobData: "JobData",
|
|
||||||
DMSVid: "DMSVid",
|
|
||||||
DMSVeh: "DMSVeh",
|
|
||||||
DMSVehCustomer: "DMSVehCustomer",
|
|
||||||
DMSCustList: "DMSCustList",
|
|
||||||
DMSCust: "DMSCust",
|
|
||||||
selectedCustomerId: "selectedCustomerId",
|
|
||||||
DMSTransHeader: "DMSTransHeader",
|
|
||||||
transWips: "transWips",
|
|
||||||
DmsBatchTxnPost: "DmsBatchTxnPost",
|
|
||||||
DMSVehHistory: "DMSVehHistory"
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provider-level token cache.
|
|
||||||
* We reuse redisHelpers.setSessionData/getSessionData with a synthetic "socketId"
|
|
||||||
* so we don’t need direct access to the Redis client here.
|
|
||||||
*/
|
|
||||||
const RR_PROVIDER_TOKEN_BUCKET = "rr:provider-token"; // becomes key: "socket:rr:provider-token"
|
|
||||||
const RR_PROVIDER_TOKEN_FIELD = "token";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch an RR access token.
|
|
||||||
* TODO: Implement the *actual* RR auth flow per the spec (client credentials
|
|
||||||
* or whatever RCI requires). This stub uses an env or a fixed dev token.
|
|
||||||
*
|
|
||||||
* @param {Object} deps
|
|
||||||
* @param {Object} deps.redisHelpers - Your redisHelpers API
|
|
||||||
* @returns {Promise<string>} accessToken
|
|
||||||
*/
|
|
||||||
async function getRRToken({ redisHelpers }) {
|
|
||||||
try {
|
|
||||||
// Try the cache first
|
|
||||||
const cached = await redisHelpers.getSessionData(RR_PROVIDER_TOKEN_BUCKET, RR_PROVIDER_TOKEN_FIELD);
|
|
||||||
if (cached?.accessToken && cached?.expiresAt && Date.now() < cached.expiresAt - 5000) {
|
|
||||||
return cached.accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Replace with real RR auth call. For now, fallback to env.
|
|
||||||
const accessToken = process.env.RR_FAKE_TOKEN || "rr-dev-token";
|
|
||||||
// Artificial ~55m expiry (adjust to actual token TTL)
|
|
||||||
const expiresAt = Date.now() + 55 * 60 * 1000;
|
|
||||||
|
|
||||||
await redisHelpers.setSessionData(
|
|
||||||
RR_PROVIDER_TOKEN_BUCKET,
|
|
||||||
RR_PROVIDER_TOKEN_FIELD,
|
|
||||||
{ accessToken, expiresAt },
|
|
||||||
60 * 60 // TTL safety net
|
|
||||||
);
|
|
||||||
|
|
||||||
return accessToken;
|
|
||||||
} catch (error) {
|
|
||||||
logger.log("rr-get-token-error", "ERROR", "api", "rr", {
|
|
||||||
message: error?.message,
|
|
||||||
stack: error?.stack
|
|
||||||
});
|
|
||||||
// Keep local dev moving even if cache errors
|
|
||||||
return process.env.RR_FAKE_TOKEN || "rr-dev-token";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a full URL including optional path segment and query params.
|
* Helper to retrieve combined configuration (env + dealer) for calls.
|
||||||
* Matches the function signature used elsewhere in the codebase.
|
* NOTE: This does not hit Redis. DB only (dealer overrides) + env secrets.
|
||||||
*
|
* @param {object} socket - Either a real socket or an Express req carrying bodyshopId on .bodyshopId
|
||||||
* @param {Object} args
|
* @returns {Promise<object>} configuration
|
||||||
* @param {string} args.url - base URL (may or may not end with "/")
|
|
||||||
* @param {string} [args.pathParams] - string appended to URL (no leading slash)
|
|
||||||
* @param {Array<[string,string]>} [args.requestSearchParams] - pairs converted to query params
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) {
|
async function resolveRRConfig(socket) {
|
||||||
// normalize: ensure exactly one trailing slash on base
|
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
||||||
url = url.replace(/\/+$/, "/");
|
const dealerCfg = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||||
const fullPath = pathParams ? `${url}${pathParams}` : url;
|
return { ...getBaseRRConfig(), ...dealerCfg };
|
||||||
const query = new URLSearchParams(requestSearchParams).toString();
|
}
|
||||||
return query ? `${fullPath}?${query}` : fullPath;
|
|
||||||
|
/* ------------------------------------------------------------------------------------------------
|
||||||
|
* Template rendering
|
||||||
|
* ----------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and renders a Mustache XML template with provided data.
|
||||||
|
* @param {string} templateName - Name of XML file under server/rr/xml-templates/ (without .xml)
|
||||||
|
* @param {object} data - Template substitution object
|
||||||
|
* @returns {Promise<string>} Rendered XML string
|
||||||
|
*/
|
||||||
|
async function renderXmlTemplate(templateName, data) {
|
||||||
|
const templatePath = path.join(__dirname, "xml-templates", `${templateName}.xml`);
|
||||||
|
const xmlTemplate = await fs.readFile(templatePath, "utf8");
|
||||||
|
return mustache.render(xmlTemplate, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional delayed/batch polling flow (placeholder).
|
* Build a SOAP envelope with a rendered header + body.
|
||||||
* If RR returns a "check later" envelope, use this to poll until "complete".
|
* Header comes from xml-templates/_EnvelopeHeader.xml.
|
||||||
* Adjust the header names and result shapes once you have the real spec.
|
* @param {string} renderedBodyXml
|
||||||
*
|
* @param {object} headerVars - values for header mustache
|
||||||
* @param {Object} args
|
* @returns {Promise<string>}
|
||||||
* @param {Object} args.delayMeta - body returned from initial RR call with status link(s)
|
|
||||||
* @param {string} args.access_token - token to reuse for polling
|
|
||||||
* @param {string} args.reqId - correlation id
|
|
||||||
* @returns {Promise<any>}
|
|
||||||
*/
|
*/
|
||||||
async function DelayedCallback({ delayMeta, access_token, reqId }) {
|
async function buildSoapEnvelopeWithHeader(renderedBodyXml, headerVars) {
|
||||||
// Stub example — adapt to RR if they do a batch/status-result pattern
|
const headerXml = await renderXmlTemplate("_EnvelopeHeader", headerVars);
|
||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
|
||||||
await sleep((delayMeta?.checkStatusAfterSeconds || 2) * 1000);
|
|
||||||
|
|
||||||
const statusUrl = delayMeta?._links?.status?.href;
|
return `
|
||||||
if (!statusUrl) {
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||||
return { error: "No status URL provided by RR batch envelope." };
|
<soapenv:Header>
|
||||||
}
|
${headerXml}
|
||||||
|
</soapenv:Header>
|
||||||
|
<soapenv:Body>
|
||||||
|
${renderedBodyXml}
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const statusResult = await axios.get(statusUrl, {
|
/* ------------------------------------------------------------------------------------------------
|
||||||
headers: {
|
* Core SOAP caller
|
||||||
Authorization: `Bearer ${access_token}`,
|
* ----------------------------------------------------------------------------------------------*/
|
||||||
"X-Request-Id": reqId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (statusResult?.data?.status === "complete") {
|
/**
|
||||||
const resultUrl = statusResult?.data?._links?.result?.href;
|
* Compute the full URL and SOAPAction for a given action spec.
|
||||||
if (!resultUrl) return statusResult.data;
|
* Allows either:
|
||||||
const batchResult = await axios.get(resultUrl, {
|
* - action: a key into RR_ACTIONS (e.g. "GetAdvisors")
|
||||||
headers: {
|
* - action: a raw URL/spec
|
||||||
Authorization: `Bearer ${access_token}`,
|
*/
|
||||||
"X-Request-Id": reqId
|
function resolveActionTarget(action, baseUrl) {
|
||||||
}
|
if (typeof action === "string" && RR_ACTIONS[action]) {
|
||||||
});
|
const spec = RR_ACTIONS[action];
|
||||||
return batchResult.data;
|
const soapAction = spec.soapAction || spec.action || action;
|
||||||
}
|
const cleanedBase = (spec.baseUrl || baseUrl || "").replace(/\/+$/, "");
|
||||||
|
const url = spec.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
|
||||||
|
return { url, soapAction };
|
||||||
}
|
}
|
||||||
return { error: "Batch result still not complete after max attempts." };
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(ms) {
|
if (action && typeof action === "object" && (action.url || action.soapAction || action.action)) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
const soapAction = action.soapAction || action.action || "";
|
||||||
|
const cleanedBase = (action.baseUrl || baseUrl || "").replace(/\/+$/, "");
|
||||||
|
const url = action.url || (soapAction ? `${cleanedBase}/${soapAction}` : cleanedBase);
|
||||||
|
return { url, soapAction };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof action === "string") {
|
||||||
|
return { url: action, soapAction: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid RR action. Must be a known RR_ACTIONS key, an action spec, or a URL string.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core caller. Mirrors Fortellis' MakeFortellisCall shape so we can reuse flow.
|
* Constructs and sends a SOAP call to the Reynolds & Reynolds endpoint.
|
||||||
*
|
*
|
||||||
* @param {Object} args
|
* Body can be one of:
|
||||||
* @param {string} args.apiName - logical name (used in logs/errors)
|
* - string (already-rendered XML body)
|
||||||
* @param {string} args.url - base endpoint
|
* - { template: "TemplateName", data: {...} } to render server/rr/xml-templates/TemplateName.xml
|
||||||
* @param {Object} [args.headers] - extra headers to send
|
*
|
||||||
* @param {Object} [args.body] - POST/PUT body
|
* @param {object} params
|
||||||
* @param {"get"|"post"|"put"|"delete"} [args.type="post"]
|
* @param {string|object} params.action - RR action key (RR_ACTIONS) or a raw URL/spec
|
||||||
* @param {boolean} [args.debug=true]
|
* @param {string|{template:string,data:object}} params.body - Rendered XML or template descriptor
|
||||||
* @param {string} [args.requestPathParams] - path segment to append to url
|
* @param {object} params.socket - The socket or req object for context (used to resolve config & logging)
|
||||||
* @param {Array<[string,string]>} [args.requestSearchParams=[]] - tuples of [key, val] for query params
|
* @param {object} [params.redisHelpers]
|
||||||
* @param {string|number} [args.jobid] - used for logger correlation (optional)
|
* @param {string|number} [params.jobid]
|
||||||
* @param {Object} args.redisHelpers - your redisHelpers api (for token cache)
|
* @param {object} [params.dealerConfig]
|
||||||
* @param {Object} [args.socket] - pass-through so we can pull user/email if needed
|
* @param {number} [params.retries=1]
|
||||||
* @returns {Promise<any>}
|
* @returns {Promise<string>} Raw SOAP response text
|
||||||
*/
|
*/
|
||||||
async function MakeRRCall({
|
async function MakeRRCall({
|
||||||
apiName,
|
action,
|
||||||
url,
|
body,
|
||||||
headers = {},
|
socket,
|
||||||
body = {},
|
// redisHelpers,
|
||||||
type = "post",
|
|
||||||
debug = true,
|
|
||||||
requestPathParams,
|
|
||||||
requestSearchParams = [],
|
|
||||||
jobid,
|
jobid,
|
||||||
redisHelpers,
|
dealerConfig,
|
||||||
socket
|
retries = 1
|
||||||
}) {
|
}) {
|
||||||
const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams });
|
const correlationId = uuidv4();
|
||||||
const reqId = uuid();
|
|
||||||
const idempotencyKey = uuid();
|
|
||||||
const access_token = await getRRToken({ redisHelpers });
|
|
||||||
|
|
||||||
if (debug) {
|
const effectiveConfig = dealerConfig || (await resolveRRConfig(socket));
|
||||||
logger.log("rr-call", "DEBUG", socket?.user?.email, null, {
|
const { url, soapAction } = resolveActionTarget(action, effectiveConfig.baseUrl);
|
||||||
apiName,
|
|
||||||
type,
|
// Render body if given by template descriptor
|
||||||
url: fullUrl,
|
let renderedBody = body;
|
||||||
jobid,
|
if (body && typeof body === "object" && body.template) {
|
||||||
reqId,
|
renderedBody = await renderXmlTemplate(body.template, body.data || {});
|
||||||
body: safeLogJson(body)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Build header vars (from env + rr_configuration)
|
||||||
const baseHeaders = {
|
const headerVars = {
|
||||||
Authorization: `Bearer ${access_token}`,
|
PPSysId: effectiveConfig.ppsysid || process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID,
|
||||||
"X-Request-Id": reqId,
|
DealerNumber: effectiveConfig.dealer_number || effectiveConfig.dealer_id || process.env.RR_DEALER_NUMBER,
|
||||||
"Idempotency-Key": idempotencyKey,
|
StoreNumber: effectiveConfig.store_number || process.env.RR_STORE_NUMBER,
|
||||||
...headers
|
BranchNumber: effectiveConfig.branch_number || process.env.RR_BRANCH_NUMBER,
|
||||||
};
|
Username: effectiveConfig.username || process.env.RR_API_USER || process.env.RR_USERNAME,
|
||||||
|
Password: effectiveConfig.password || process.env.RR_API_PASS || process.env.RR_PASSWORD,
|
||||||
|
CorrelationId: correlationId
|
||||||
|
};
|
||||||
|
|
||||||
let resp;
|
// Build full SOAP envelope with proper header
|
||||||
switch ((type || "post").toLowerCase()) {
|
const soapEnvelope = await buildSoapEnvelopeWithHeader(renderedBody, headerVars);
|
||||||
case "get":
|
|
||||||
resp = await axios.get(fullUrl, { headers: baseHeaders });
|
|
||||||
break;
|
|
||||||
case "put":
|
|
||||||
resp = await axios.put(fullUrl, body, { headers: baseHeaders });
|
|
||||||
break;
|
|
||||||
case "delete":
|
|
||||||
// Some APIs require body with DELETE; axios supports { data } for that
|
|
||||||
resp = await axios.delete(fullUrl, { headers: baseHeaders, data: body });
|
|
||||||
break;
|
|
||||||
case "post":
|
|
||||||
default:
|
|
||||||
resp = await axios.post(fullUrl, body, { headers: baseHeaders });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug) {
|
RRLogger(socket, "info", `RR → ${soapAction || "SOAP"} request`, {
|
||||||
logger.log("rr-response", "DEBUG", socket?.user?.email, null, {
|
jobid,
|
||||||
apiName,
|
url,
|
||||||
reqId,
|
correlationId
|
||||||
data: safeLogJson(resp?.data)
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If RR returns a "check later" envelope, route through DelayedCallback
|
const headers = {
|
||||||
if (resp?.data?.checkStatusAfterSeconds) {
|
...RR_SOAP_HEADERS,
|
||||||
const delayed = await DelayedCallback({
|
SOAPAction: soapAction,
|
||||||
delayMeta: resp.data,
|
"Content-Type": "text/xml; charset=utf-8",
|
||||||
access_token,
|
"X-Request-Id": correlationId
|
||||||
reqId
|
};
|
||||||
});
|
|
||||||
return delayed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp?.data;
|
let attempt = 0;
|
||||||
} catch (error) {
|
while (attempt <= retries) {
|
||||||
// Handle 429 backoff hint (simple single-retry stub)
|
attempt += 1;
|
||||||
if (error?.response?.status === 429) {
|
try {
|
||||||
const retryAfter = Number(error.response.headers?.["retry-after"] || 1);
|
const response = await axios.post(url, soapEnvelope, {
|
||||||
await sleep(retryAfter * 1000);
|
|
||||||
return MakeRRCall({
|
|
||||||
apiName,
|
|
||||||
url,
|
|
||||||
headers,
|
headers,
|
||||||
body,
|
timeout: effectiveConfig.timeout || 30000,
|
||||||
type,
|
responseType: "text",
|
||||||
debug,
|
validateStatus: () => true
|
||||||
requestPathParams,
|
|
||||||
requestSearchParams,
|
|
||||||
jobid,
|
|
||||||
redisHelpers,
|
|
||||||
socket
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const text = response.data;
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
RRLogger(socket, "error", `RR HTTP ${response.status} on ${soapAction || url}`, {
|
||||||
|
status: response.status,
|
||||||
|
jobid,
|
||||||
|
correlationId,
|
||||||
|
snippet: text?.slice?.(0, 512)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status >= 500 && attempt <= retries) {
|
||||||
|
RRLogger(socket, "warn", `RR transient HTTP error; retrying (${attempt}/${retries})`, {
|
||||||
|
correlationId
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`RR HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", `RR ← ${soapAction || "SOAP"} response`, {
|
||||||
|
jobid,
|
||||||
|
correlationId,
|
||||||
|
bytes: Buffer.byteLength(text || "", "utf8")
|
||||||
|
});
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} catch (err) {
|
||||||
|
const transient = /ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up|network error/i.test(
|
||||||
|
err?.message || ""
|
||||||
|
);
|
||||||
|
if (transient && attempt <= retries) {
|
||||||
|
RRLogger(socket, "warn", `RR transient network error; retrying (${attempt}/${retries})`, {
|
||||||
|
error: err.message,
|
||||||
|
correlationId
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RRLogger(socket, "error", `RR ${soapAction || "SOAP"} failed`, {
|
||||||
|
error: err.message,
|
||||||
|
jobid,
|
||||||
|
correlationId
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errPayload = {
|
|
||||||
reqId,
|
|
||||||
url: fullUrl,
|
|
||||||
apiName,
|
|
||||||
errorData: error?.response?.data,
|
|
||||||
status: error?.response?.status,
|
|
||||||
statusText: error?.response?.statusText
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log and throw a typed error (consistent with Fortellis helpers)
|
|
||||||
logger.log("rr-call-error", "ERROR", socket?.user?.email, null, {
|
|
||||||
...errPayload,
|
|
||||||
message: error?.message,
|
|
||||||
stack: error?.stack
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new RrApiError(`RR API call failed for ${apiName}: ${error?.message}`, errPayload);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/* ------------------------------------------------------------------------------------------------
|
||||||
* Central action registry.
|
* Exports
|
||||||
* TODO: Replace ALL URLs with real RR endpoints from the Rome/RR specs.
|
* ----------------------------------------------------------------------------------------------*/
|
||||||
* You can later split into domain-specific registries if it grows large.
|
|
||||||
*/
|
|
||||||
const RRActions = {
|
|
||||||
// Vehicles
|
|
||||||
GetVehicleId: {
|
|
||||||
apiName: "RR Get Vehicle Id",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/service-vehicle-mgmt/v1/vehicle-ids/" // append VIN
|
|
||||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/vehicle-ids/",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
ReadVehicle: {
|
|
||||||
apiName: "RR Read Vehicle",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/service-vehicle-mgmt/v1/" // append vehicleId
|
|
||||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
InsertVehicle: {
|
|
||||||
apiName: "RR Insert Service Vehicle",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/service-vehicle-mgmt/v1/"
|
|
||||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/",
|
|
||||||
type: "post"
|
|
||||||
},
|
|
||||||
UpdateVehicle: {
|
|
||||||
apiName: "RR Update Service Vehicle",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/service-vehicle-mgmt/v1/"
|
|
||||||
: "https://rr-uat.example.com/service-vehicle-mgmt/v1/",
|
|
||||||
type: "put"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Customers
|
const RRActions = RR_ACTIONS;
|
||||||
CreateCustomer: {
|
|
||||||
apiName: "RR Create Customer",
|
|
||||||
url: isProduction ? "https://rr.example.com/customer/v1/" : "https://rr-uat.example.com/customer/v1/",
|
|
||||||
type: "post"
|
|
||||||
},
|
|
||||||
UpdateCustomer: {
|
|
||||||
apiName: "RR Update Customer",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/customer/v1/" // append /{id} if required by spec
|
|
||||||
: "https://rr-uat.example.com/customer/v1/",
|
|
||||||
type: "put"
|
|
||||||
},
|
|
||||||
ReadCustomer: {
|
|
||||||
apiName: "RR Read Customer",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/customer/v1/" // append /{id}
|
|
||||||
: "https://rr-uat.example.com/customer/v1/",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
SearchCustomer: {
|
|
||||||
apiName: "RR Query Customer By Name",
|
|
||||||
url: isProduction ? "https://rr.example.com/customer/v1/search" : "https://rr-uat.example.com/customer/v1/search",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Combined search (customer + vehicle)
|
|
||||||
CombinedSearch: {
|
|
||||||
apiName: "RR Combined Search (Customer + Vehicle)",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/search/v1/customer-vehicle"
|
|
||||||
: "https://rr-uat.example.com/search/v1/customer-vehicle",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Advisors
|
|
||||||
GetAdvisors: {
|
|
||||||
apiName: "RR Get Advisors",
|
|
||||||
url: isProduction ? "https://rr.example.com/advisors/v1" : "https://rr-uat.example.com/advisors/v1",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Parts
|
|
||||||
GetParts: {
|
|
||||||
apiName: "RR Get Parts",
|
|
||||||
url: isProduction ? "https://rr.example.com/parts/v1" : "https://rr-uat.example.com/parts/v1",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
|
|
||||||
// GL / WIP (mirroring your existing flows; endpoints are placeholders)
|
|
||||||
StartWip: {
|
|
||||||
apiName: "RR Start WIP",
|
|
||||||
url: isProduction ? "https://rr.example.com/glpost/v1/startWIP" : "https://rr-uat.example.com/glpost/v1/startWIP",
|
|
||||||
type: "post"
|
|
||||||
},
|
|
||||||
TranBatchWip: {
|
|
||||||
apiName: "RR Trans Batch WIP",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/glpost/v1/transBatchWIP"
|
|
||||||
: "https://rr-uat.example.com/glpost/v1/transBatchWIP",
|
|
||||||
type: "post"
|
|
||||||
},
|
|
||||||
PostBatchWip: {
|
|
||||||
apiName: "RR Post Batch WIP",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/glpost/v1/postBatchWIP"
|
|
||||||
: "https://rr-uat.example.com/glpost/v1/postBatchWIP",
|
|
||||||
type: "post"
|
|
||||||
},
|
|
||||||
QueryErrorWip: {
|
|
||||||
apiName: "RR Query Error WIP",
|
|
||||||
url: isProduction ? "https://rr.example.com/glpost/v1/errWIP" : "https://rr-uat.example.com/glpost/v1/errWIP",
|
|
||||||
type: "get"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Service history (header insert)
|
|
||||||
ServiceHistoryInsert: {
|
|
||||||
apiName: "RR Service Vehicle History Insert",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/service-vehicle-history-mgmt/v1/"
|
|
||||||
: "https://rr-uat.example.com/service-vehicle-history-mgmt/v1/",
|
|
||||||
type: "post"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Repair Orders
|
|
||||||
CreateRepairOrder: {
|
|
||||||
apiName: "RR Create Repair Order",
|
|
||||||
url: isProduction ? "https://rr.example.com/repair-orders/v1" : "https://rr-uat.example.com/repair-orders/v1",
|
|
||||||
type: "post"
|
|
||||||
},
|
|
||||||
UpdateRepairOrder: {
|
|
||||||
apiName: "RR Update Repair Order",
|
|
||||||
url: isProduction
|
|
||||||
? "https://rr.example.com/repair-orders/v1/" // append /{id} if required
|
|
||||||
: "https://rr-uat.example.com/repair-orders/v1/",
|
|
||||||
type: "put"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safe JSON logger helper to avoid huge payloads/recursive structures in logs.
|
|
||||||
*/
|
|
||||||
function safeLogJson(data) {
|
|
||||||
try {
|
|
||||||
const text = JSON.stringify(data);
|
|
||||||
// cap to ~5k for logs
|
|
||||||
return text.length > 5000 ? `${text.slice(0, 5000)}… [truncated]` : text;
|
|
||||||
} catch {
|
|
||||||
return "[unserializable]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// core helpers
|
|
||||||
MakeRRCall,
|
MakeRRCall,
|
||||||
RRActions,
|
getDealerConfig,
|
||||||
getRRToken,
|
renderXmlTemplate,
|
||||||
constructFullUrl,
|
resolveRRConfig,
|
||||||
DelayedCallback,
|
RRActions
|
||||||
|
|
||||||
// parity exports required by other RR modules
|
|
||||||
getTransactionType,
|
|
||||||
defaultRRTTL,
|
|
||||||
RRCacheEnums
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,567 +1,133 @@
|
|||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// Reynolds & Reynolds (RR) Job Export flow (scaffold).
|
* @file rr-job-export.js
|
||||||
//
|
* @description Orchestrates the full Reynolds & Reynolds DMS export flow.
|
||||||
// Parity with Fortellis/CDK export shape so the UI + socket flows remain
|
* Creates/updates customers, vehicles, and repair orders according to Rome specs.
|
||||||
// consistent:
|
*/
|
||||||
//
|
|
||||||
// - RRJobExport: initial VIN/customer discovery & prompt for customer select
|
|
||||||
// - RRSelectedCustomer: create/update customer, insert/read vehicle,
|
|
||||||
// post WIP batch, post history, mark success/failure, notify client
|
|
||||||
//
|
|
||||||
// What’s still missing (fill in from Rome/RR PDFs you provided):
|
|
||||||
// - Exact request/response envelopes for each RR operation
|
|
||||||
// (Customer Insert/Update, Vehicle Insert/Read, WIP APIs, Service History).
|
|
||||||
// - Final success/error conditions for assertRrOk (we currently use heuristics).
|
|
||||||
// - Precise field mappings inside CreateCustomer, InsertVehicle,
|
|
||||||
// StartWip/TransBatchWip/PostBatchWip, InsertServiceVehicleHistory.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const { GraphQLClient } = require("graphql-request");
|
const { RrCustomerInsert, RrCustomerUpdate } = require("./rr-customer");
|
||||||
const moment = require("moment-timezone");
|
const { CreateRepairOrder, UpdateRepairOrder } = require("./rr-repair-orders");
|
||||||
|
const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers");
|
||||||
const CalculateAllocations = require("../cdk/cdk-calculate-allocations").default; // reuse allocations
|
const { assertRrOkXml, extractRrResponseData } = require("./rr-error");
|
||||||
const CreateRRLogEvent = require("./rr-logger");
|
const RRLogger = require("./rr-logger");
|
||||||
const queries = require("../graphql-client/queries");
|
const { mapServiceVehicleInsert } = require("./rr-mappers");
|
||||||
const { MakeRRCall, RRActions, getTransactionType, defaultRRTTL, RRCacheEnums } = require("./rr-helpers");
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// Public entry points (wired in redisSocketEvents.js)
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seed export: cache txEnvelope + JobData, discover VIN->VehicleId + owner,
|
* Inserts a service vehicle record for the repair order.
|
||||||
* search by customer name, and prompt client to select/create a customer.
|
* Follows the "Rome Insert Service Vehicle Interface Specification" via SOAP/XML.
|
||||||
*/
|
*/
|
||||||
async function RRJobExport({ socket, redisHelpers, txEnvelope, jobid }) {
|
async function RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig }) {
|
||||||
const { setSessionTransactionData } = redisHelpers;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CreateRRLogEvent(socket, "DEBUG", `[RR] Received Job export request`, { jobid });
|
RRLogger(socket, "info", "RR Insert Service Vehicle started", { jobid: JobData?.id });
|
||||||
|
|
||||||
// cache txEnvelope for this job session
|
// Build Mustache variables for server/rr/xml-templates/InsertServiceVehicle.xml
|
||||||
await setSessionTransactionData(
|
const variables = mapServiceVehicleInsert(JobData, dealerConfig);
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.txEnvelope,
|
|
||||||
txEnvelope,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
|
|
||||||
const JobData = await QueryJobData({ socket, jobid });
|
const xml = await MakeRRCall({
|
||||||
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData, JobData, defaultRRTTL);
|
action: RRActions.InsertServiceVehicle,
|
||||||
|
body: { template: "InsertServiceVehicle", data: variables },
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid: JobData.id,
|
||||||
|
dealerConfig
|
||||||
|
});
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "DEBUG", `[RR] Get Vehicle Id via VIN`, { vin: JobData.v_vin });
|
const ok = assertRrOkXml(xml, { apiName: "RR Insert Service Vehicle" });
|
||||||
|
const normalized = extractRrResponseData(ok, { action: "InsertServiceVehicle" });
|
||||||
|
|
||||||
const DMSVid = await GetVehicleId({ socket, redisHelpers, JobData });
|
RRLogger(socket, "debug", "RR Insert Service Vehicle success", {
|
||||||
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVid, DMSVid, defaultRRTTL);
|
jobid: JobData?.id,
|
||||||
|
vehicleId: normalized?.VehicleId || normalized?.vehicleId
|
||||||
|
});
|
||||||
|
|
||||||
let DMSVehCustomer;
|
return normalized;
|
||||||
if (!DMSVid?.newId) {
|
|
||||||
// existing vehicle, load details
|
|
||||||
const DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId });
|
|
||||||
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL);
|
|
||||||
|
|
||||||
// Try to read the CURRENT owner (shape TBD per RR)
|
|
||||||
const owner = DMSVeh?.owners && DMSVeh.owners.find((o) => o.id?.assigningPartyId === "CURRENT");
|
|
||||||
if (owner?.id?.value) {
|
|
||||||
DMSVehCustomer = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: owner.id.value });
|
|
||||||
await setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.DMSVehCustomer,
|
|
||||||
DMSVehCustomer,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search customers by job owner name (param names TBD per RR)
|
|
||||||
const DMSCustList = await SearchCustomerByName({ socket, redisHelpers, JobData });
|
|
||||||
await setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.DMSCustList,
|
|
||||||
DMSCustList,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
|
|
||||||
// Emit choices: (VIN owner first if present) + search results
|
|
||||||
socket.emit("rr-select-customer", [
|
|
||||||
...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
|
|
||||||
...(Array.isArray(DMSCustList) ? DMSCustList : [])
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CreateRRLogEvent(socket, "ERROR", `[RR] RRJobExport failed: ${error.message}`, { stack: error.stack });
|
RRLogger(socket, "error", `RR Insert Service Vehicle failed: ${error.message}`, { jobid: JobData?.id });
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After client selects a customer (or requests create):
|
* Full DMS export sequence for Reynolds & Reynolds.
|
||||||
* - Read or create the customer
|
|
||||||
* - Insert vehicle if needed (or read existing)
|
|
||||||
* - StartWip -> TransBatchWip -> PostBatchWip -> Mark exported
|
|
||||||
* - Optionally insert service history
|
|
||||||
*/
|
|
||||||
async function RRSelectedCustomer({ socket, redisHelpers, selectedCustomerId, jobid }) {
|
|
||||||
const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.selectedCustomerId,
|
|
||||||
selectedCustomerId,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
|
|
||||||
const JobData = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData);
|
|
||||||
const txEnvelope = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.txEnvelope);
|
|
||||||
const DMSVid = await getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVid);
|
|
||||||
|
|
||||||
// Ensure we have a customer to use
|
|
||||||
let DMSCust;
|
|
||||||
if (selectedCustomerId) {
|
|
||||||
DMSCust = await ReadCustomerById({ socket, redisHelpers, JobData, customerId: selectedCustomerId });
|
|
||||||
} else {
|
|
||||||
const createRes = await CreateCustomer({ socket, redisHelpers, JobData });
|
|
||||||
DMSCust = { customerId: createRes?.data || createRes?.customerId || createRes?.id };
|
|
||||||
}
|
|
||||||
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSCust, DMSCust, defaultRRTTL);
|
|
||||||
|
|
||||||
// Ensure the vehicle exists (ownership model TBD per RR)
|
|
||||||
let DMSVeh;
|
|
||||||
if (DMSVid?.newId) {
|
|
||||||
DMSVeh = await InsertVehicle({ socket, redisHelpers, JobData, txEnvelope, DMSVid, DMSCust });
|
|
||||||
} else {
|
|
||||||
DMSVeh = await ReadVehicleById({ socket, redisHelpers, JobData, vehicleId: DMSVid.vehiclesVehId });
|
|
||||||
// TODO: If RR supports “UpdateVehicle” to change ownership, add it here.
|
|
||||||
}
|
|
||||||
await setSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.DMSVeh, DMSVeh, defaultRRTTL);
|
|
||||||
|
|
||||||
// Start WIP header
|
|
||||||
const DMSTransHeader = await StartWip({ socket, redisHelpers, JobData, txEnvelope });
|
|
||||||
await setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.DMSTransHeader,
|
|
||||||
DMSTransHeader,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
|
|
||||||
// Post lines
|
|
||||||
const DMSBatchTxn = await TransBatchWip({ socket, redisHelpers, JobData });
|
|
||||||
await setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.DMSBatchTxn,
|
|
||||||
DMSBatchTxn,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decide success from envelope (heuristic until exact spec confirmed)
|
|
||||||
if (String(DMSBatchTxn?.rtnCode || "0") === "0") {
|
|
||||||
const DmsBatchTxnPost = await PostBatchWip({ socket, redisHelpers, JobData });
|
|
||||||
await setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.DmsBatchTxnPost,
|
|
||||||
DmsBatchTxnPost,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
|
|
||||||
if (String(DmsBatchTxnPost?.rtnCode || "0") === "0") {
|
|
||||||
await MarkJobExported({ socket, jobid: JobData.id, redisHelpers });
|
|
||||||
|
|
||||||
// Optional service history write (non-blocking)
|
|
||||||
try {
|
|
||||||
const DMSVehHistory = await InsertServiceVehicleHistory({ socket, redisHelpers, JobData });
|
|
||||||
await setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.DMSVehHistory,
|
|
||||||
DMSVehHistory,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
CreateRRLogEvent(socket, "WARN", `[RR] ServiceVehicleHistory optional step failed: ${e.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit("export-success", JobData.id);
|
|
||||||
} else {
|
|
||||||
await HandlePostingError({ socket, redisHelpers, JobData, DMSTransHeader });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await InsertFailedExportLog({
|
|
||||||
socket,
|
|
||||||
JobData,
|
|
||||||
error: `RR DMSBatchTxn not successful: ${JSON.stringify(DMSBatchTxn)}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `[RR] RRSelectedCustomer failed: ${error.message}`, { stack: error.stack });
|
|
||||||
const JobData = await redisHelpers.getSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.JobData
|
|
||||||
);
|
|
||||||
if (JobData) await InsertFailedExportLog({ socket, JobData, error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// GraphQL job fetch
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function QueryJobData({ socket, jobid }) {
|
|
||||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
|
||||||
const currentToken =
|
|
||||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
|
||||||
|
|
||||||
const result = await client
|
|
||||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
|
||||||
.request(queries.QUERY_JOBS_FOR_CDK_EXPORT, { id: jobid });
|
|
||||||
|
|
||||||
return result.jobs_by_pk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// RR API step stubs (wire to MakeRRCall). Replace request payloads once the
|
|
||||||
// exact RR/Rome schemas are confirmed from the PDFs.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function GetVehicleId({ socket, redisHelpers, JobData }) {
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.GetVehicleId,
|
|
||||||
requestPathParams: JobData.v_vin,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ReadVehicleById({ socket, redisHelpers, JobData, vehicleId }) {
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.ReadVehicle,
|
|
||||||
requestPathParams: vehicleId,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ReadCustomerById({ socket, redisHelpers, JobData, customerId }) {
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.ReadCustomer,
|
|
||||||
requestPathParams: customerId,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function SearchCustomerByName({ socket, redisHelpers, JobData }) {
|
|
||||||
// TODO: Confirm exact query param names from the RR search spec
|
|
||||||
const ownerNameParams =
|
|
||||||
JobData.ownr_co_nm && JobData.ownr_co_nm.trim() !== ""
|
|
||||||
? [["lastName", JobData.ownr_co_nm]] // placeholder: business search
|
|
||||||
: [
|
|
||||||
["firstName", JobData.ownr_fn],
|
|
||||||
["lastName", JobData.ownr_ln]
|
|
||||||
];
|
|
||||||
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.QueryCustomerByName, // ✅ use action defined in rr-helpers
|
|
||||||
requestSearchParams: ownerNameParams,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function CreateCustomer({ socket, redisHelpers, JobData }) {
|
|
||||||
// TODO: Replace with exact RR Customer Insert envelope & fields
|
|
||||||
const body = {
|
|
||||||
customerType: JobData.ownr_co_nm ? "BUSINESS" : "INDIVIDUAL"
|
|
||||||
};
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.CreateCustomer,
|
|
||||||
body,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InsertVehicle({ socket, redisHelpers, JobData /*, txEnvelope, DMSVid, DMSCust*/ }) {
|
|
||||||
// TODO: Replace with exact RR Service Vehicle Insert mapping
|
|
||||||
const body = {
|
|
||||||
vin: JobData.v_vin
|
|
||||||
// owners, make/model, odometer, etc…
|
|
||||||
};
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.InsertVehicle,
|
|
||||||
body,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function StartWip({ socket, redisHelpers, JobData, txEnvelope }) {
|
|
||||||
// TODO: Replace body fields with RR WIP header schema
|
|
||||||
const body = {
|
|
||||||
acctgDate: moment().tz(JobData.bodyshop.timezone).format("YYYY-MM-DD"),
|
|
||||||
desc: txEnvelope?.story || "",
|
|
||||||
docType: "10",
|
|
||||||
m13Flag: "0",
|
|
||||||
refer: JobData.ro_number,
|
|
||||||
srcCo: JobData.bodyshop?.cdk_configuration?.srcco || "00", // placeholder from CDK config; RR equivalent TBD
|
|
||||||
srcJrnl: txEnvelope?.journal,
|
|
||||||
userID: "BSMS"
|
|
||||||
};
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.StartWip,
|
|
||||||
body,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function TransBatchWip({ socket, redisHelpers, JobData }) {
|
|
||||||
const wips = await GenerateTransWips({ socket, redisHelpers, JobData });
|
|
||||||
|
|
||||||
// TODO: Ensure this body shape matches RR batch transaction schema
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.TranBatchWip,
|
|
||||||
body: wips,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function PostBatchWip({ socket, redisHelpers, JobData }) {
|
|
||||||
const DMSTransHeader = await redisHelpers.getSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(JobData.id),
|
|
||||||
RRCacheEnums.DMSTransHeader
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Confirm final field names for “post” operation in RR
|
|
||||||
const body = {
|
|
||||||
opCode: "P",
|
|
||||||
transID: DMSTransHeader?.transID
|
|
||||||
};
|
|
||||||
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.PostBatchWip,
|
|
||||||
body,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function QueryErrWip({ socket, redisHelpers, JobData }) {
|
|
||||||
const DMSTransHeader = await redisHelpers.getSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(JobData.id),
|
|
||||||
RRCacheEnums.DMSTransHeader
|
|
||||||
);
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.QueryErrorWip,
|
|
||||||
requestPathParams: DMSTransHeader?.transID,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function DeleteWip({ socket, redisHelpers, JobData }) {
|
|
||||||
const DMSTransHeader = await redisHelpers.getSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(JobData.id),
|
|
||||||
RRCacheEnums.DMSTransHeader
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Confirm if RR uses the same endpoint with opCode=D to delete/void
|
|
||||||
const body = { opCode: "D", transID: DMSTransHeader?.transID };
|
|
||||||
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.PostBatchWip,
|
|
||||||
body,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InsertServiceVehicleHistory({ socket, redisHelpers, JobData }) {
|
|
||||||
const txEnvelope = await redisHelpers.getSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(JobData.id),
|
|
||||||
RRCacheEnums.txEnvelope
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Replace with RR Service Vehicle History schema
|
|
||||||
const body = {
|
|
||||||
comments: txEnvelope?.story || ""
|
|
||||||
};
|
|
||||||
return await MakeRRCall({
|
|
||||||
...RRActions.ServiceHistoryInsert,
|
|
||||||
body,
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid: JobData.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function HandlePostingError({ socket, redisHelpers, JobData /*, DMSTransHeader*/ }) {
|
|
||||||
const DmsError = await QueryErrWip({ socket, redisHelpers, JobData });
|
|
||||||
await DeleteWip({ socket, redisHelpers, JobData });
|
|
||||||
|
|
||||||
const errString = DmsError?.errMsg || JSON.stringify(DmsError);
|
|
||||||
errString?.split("|")?.forEach((e) => e && CreateRRLogEvent(socket, "ERROR", `[RR] Post error: ${e}`));
|
|
||||||
await InsertFailedExportLog({ socket, JobData, error: errString });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert app allocations to RR WIP lines.
|
|
||||||
* Re-uses existing CalculateAllocations to keep parity with CDK/Fortellis.
|
|
||||||
*
|
*
|
||||||
* TODO: Confirm exact RR posting model (accounts, control numbers, company ids,
|
* 1. Ensure customer exists (insert or update)
|
||||||
* and whether amounts are signed or need separate debit/credit flags).
|
* 2. Ensure vehicle exists/linked
|
||||||
|
* 3. Create or update repair order
|
||||||
*/
|
*/
|
||||||
async function GenerateTransWips({ socket, redisHelpers, JobData }) {
|
async function ExportJobToRR({ socket, redisHelpers, JobData }) {
|
||||||
const allocations = await CalculateAllocations(socket, JobData.id, true); // true==verbose logging
|
const jobid = JobData?.id;
|
||||||
const DMSTransHeader = await redisHelpers.getSessionTransactionData(
|
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||||
socket.id,
|
|
||||||
getTransactionType(JobData.id),
|
|
||||||
RRCacheEnums.DMSTransHeader
|
|
||||||
);
|
|
||||||
|
|
||||||
const wips = [];
|
RRLogger(socket, "info", "Starting RR job export", { jobid, bodyshopId });
|
||||||
allocations.forEach((alloc) => {
|
|
||||||
if (alloc.sale.getAmount() > 0 && !alloc.tax) {
|
|
||||||
wips.push({
|
|
||||||
acct: alloc.profitCenter.dms_acctnumber,
|
|
||||||
cntl: alloc.profitCenter.dms_control_override || JobData.ro_number,
|
|
||||||
postAmt: alloc.sale.multiply(-1).getAmount(), // sale is a credit in many GLs; confirm RR sign
|
|
||||||
transID: DMSTransHeader?.transID,
|
|
||||||
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco // RR equivalent TBD
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (alloc.cost.getAmount() > 0 && !alloc.tax) {
|
|
||||||
wips.push({
|
|
||||||
acct: alloc.costCenter.dms_acctnumber,
|
|
||||||
cntl: alloc.costCenter.dms_control_override || JobData.ro_number,
|
|
||||||
postAmt: alloc.cost.getAmount(),
|
|
||||||
transID: DMSTransHeader?.transID,
|
|
||||||
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco
|
|
||||||
});
|
|
||||||
wips.push({
|
|
||||||
acct: alloc.costCenter.dms_wip_acctnumber,
|
|
||||||
cntl: alloc.costCenter.dms_control_override || JobData.ro_number,
|
|
||||||
postAmt: alloc.cost.multiply(-1).getAmount(),
|
|
||||||
transID: DMSTransHeader?.transID,
|
|
||||||
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (alloc.tax && alloc.sale.getAmount() > 0) {
|
|
||||||
wips.push({
|
|
||||||
acct: alloc.profitCenter.dms_acctnumber,
|
|
||||||
cntl: alloc.profitCenter.dms_control_override || JobData.ro_number,
|
|
||||||
postAmt: alloc.sale.multiply(-1).getAmount(),
|
|
||||||
transID: DMSTransHeader?.transID,
|
|
||||||
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const txEnvelope = await redisHelpers.getSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(JobData.id),
|
|
||||||
RRCacheEnums.txEnvelope
|
|
||||||
);
|
|
||||||
|
|
||||||
txEnvelope?.payers?.forEach((payer) => {
|
|
||||||
wips.push({
|
|
||||||
acct: payer.dms_acctnumber,
|
|
||||||
cntl: payer.controlnumber,
|
|
||||||
postAmt: Math.round(payer.amount * 100), // assuming cents (confirm RR units)
|
|
||||||
transID: DMSTransHeader?.transID,
|
|
||||||
trgtCoID: JobData.bodyshop?.cdk_configuration?.srcco
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await redisHelpers.setSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(JobData.id),
|
|
||||||
RRCacheEnums.transWips,
|
|
||||||
wips,
|
|
||||||
defaultRRTTL
|
|
||||||
);
|
|
||||||
return wips;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// DB logging mirrors Fortellis (status + export log)
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async function MarkJobExported({ socket, jobid, redisHelpers }) {
|
|
||||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
|
||||||
const currentToken =
|
|
||||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
|
||||||
|
|
||||||
// Pull JobData from the session to get bodyshop info + default statuses
|
|
||||||
const JobData =
|
|
||||||
(await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(jobid), RRCacheEnums.JobData)) || {};
|
|
||||||
|
|
||||||
const transWips = await redisHelpers.getSessionTransactionData(
|
|
||||||
socket.id,
|
|
||||||
getTransactionType(jobid),
|
|
||||||
RRCacheEnums.transWips
|
|
||||||
);
|
|
||||||
|
|
||||||
return client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.MARK_JOB_EXPORTED, {
|
|
||||||
jobId: jobid,
|
|
||||||
job: {
|
|
||||||
status: JobData?.bodyshop?.md_ro_statuses?.default_exported || "Exported*",
|
|
||||||
date_exported: new Date()
|
|
||||||
},
|
|
||||||
log: {
|
|
||||||
bodyshopid: JobData?.bodyshop?.id,
|
|
||||||
jobid,
|
|
||||||
successful: true,
|
|
||||||
useremail: socket.user?.email,
|
|
||||||
metadata: transWips
|
|
||||||
},
|
|
||||||
bill: { exported: true, exported_at: new Date() }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function InsertFailedExportLog({ socket, JobData, error }) {
|
|
||||||
try {
|
try {
|
||||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
// Pull dealer-level overrides once (DB), env/platform secrets come from rr-helpers internally.
|
||||||
const currentToken =
|
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
|
||||||
|
|
||||||
return await client.setHeaders({ Authorization: `Bearer ${currentToken}` }).request(queries.INSERT_EXPORT_LOG, {
|
//
|
||||||
log: {
|
// STEP 1: CUSTOMER
|
||||||
bodyshopid: JobData.bodyshop.id,
|
//
|
||||||
jobid: JobData.id,
|
RRLogger(socket, "info", "RR Step 1: Customer check/insert", { jobid });
|
||||||
successful: false,
|
let rrCustomerResult;
|
||||||
message: typeof error === "string" ? error : JSON.stringify(error),
|
|
||||||
useremail: socket.user?.email
|
if (JobData?.rr_customer_id) {
|
||||||
}
|
rrCustomerResult = await RrCustomerUpdate({
|
||||||
|
socket,
|
||||||
|
redisHelpers,
|
||||||
|
JobData,
|
||||||
|
existingCustomer: { CustomerId: JobData.rr_customer_id },
|
||||||
|
patch: JobData.customer_patch
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
rrCustomerResult = await RrCustomerInsert({ socket, redisHelpers, JobData });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// STEP 2: VEHICLE
|
||||||
|
//
|
||||||
|
RRLogger(socket, "info", "RR Step 2: Vehicle insert", { jobid });
|
||||||
|
const rrVehicleResult = await RrServiceVehicleInsert({ socket, redisHelpers, JobData, dealerConfig });
|
||||||
|
|
||||||
|
//
|
||||||
|
// STEP 3: REPAIR ORDER
|
||||||
|
//
|
||||||
|
RRLogger(socket, "info", "RR Step 3: Repair Order create/update", { jobid });
|
||||||
|
let rrRepairOrderResult;
|
||||||
|
|
||||||
|
if (JobData?.rr_ro_id) {
|
||||||
|
rrRepairOrderResult = await UpdateRepairOrder({ socket, redisHelpers, JobData });
|
||||||
|
} else {
|
||||||
|
rrRepairOrderResult = await CreateRepairOrder({ socket, redisHelpers, JobData });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// FINALIZE
|
||||||
|
//
|
||||||
|
RRLogger(socket, "info", "RR Export completed successfully", {
|
||||||
|
jobid,
|
||||||
|
rr_customer_id: rrCustomerResult?.CustomerId || rrCustomerResult?.customerId,
|
||||||
|
rr_vehicle_id: rrVehicleResult?.VehicleId || rrVehicleResult?.vehicleId,
|
||||||
|
rr_ro_id: rrRepairOrderResult?.RepairOrderId || rrRepairOrderResult?.repairOrderId
|
||||||
});
|
});
|
||||||
} catch (error2) {
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error2.message}`, { stack: error2.stack });
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
customer: rrCustomerResult,
|
||||||
|
vehicle: rrVehicleResult,
|
||||||
|
repairOrder: rrRepairOrderResult
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR job export failed: ${error.message}`, { jobid });
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
RRJobExport,
|
ExportJobToRR,
|
||||||
RRSelectedCustomer
|
RrServiceVehicleInsert
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// Thin wrapper around the shared logger + a socket emitter,
|
* @file rr-logger.js
|
||||||
// mirroring the Fortellis logger shape for parity.
|
* @description Centralized logger for Reynolds & Reynolds (RR) integrations.
|
||||||
//
|
* Emits logs to CloudWatch via logger util, and back to client sockets for live visibility.
|
||||||
// Emits: "rr-log-event" { level, message, txnDetails }
|
*/
|
||||||
//
|
|
||||||
// NOTE: Keep this lightweight and side-effect free—downstream flows rely on it
|
|
||||||
// for both developer troubleshooting and UI progress messages.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit a structured RR log event to both the central logger and (if present)
|
* Create a structured RR log event.
|
||||||
* over the current socket connection for real-time UI visibility.
|
|
||||||
*
|
*
|
||||||
* @param {Object} socket - A Socket.IO socket OR an Express req (when reused in REST).
|
* @param {object} socket - The socket or Express request (both supported).
|
||||||
* Expected fields if present:
|
* @param {"debug"|"info"|"warn"|"error"} level - Log level.
|
||||||
* - socket.user?.email (used as the "actor" for audit trails)
|
* @param {string} message - Human-readable log message.
|
||||||
* - socket.emit(...) method in live socket contexts
|
* @param {object} [txnDetails] - Optional additional details (payloads, responses, etc.)
|
||||||
* @param {"SILLY"|"DEBUG"|"INFO"|"WARN"|"ERROR"} level - Log severity
|
|
||||||
* @param {string} message - Human readable message
|
|
||||||
* @param {Object} [txnDetails] - Optional structured metadata (ids, payload snippets, timings)
|
|
||||||
*/
|
*/
|
||||||
function CreateRRLogEvent(socket, level, message, txnDetails) {
|
const RRLogger = (socket, level = "info", message, txnDetails = {}) => {
|
||||||
const userEmail = socket?.user?.email || "unknown";
|
|
||||||
|
|
||||||
// 1) Centralized app logger (goes to your sinks: console, Datadog, etc.)
|
|
||||||
// Namespace: "rr-log-event" to keep provider logs grouped.
|
|
||||||
try {
|
try {
|
||||||
logger.log("rr-log-event", level, userEmail, null, {
|
// Normalize level to uppercase for CloudWatch
|
||||||
|
const levelUpper = level.toUpperCase();
|
||||||
|
|
||||||
|
// Safe email and job correlation
|
||||||
|
const userEmail =
|
||||||
|
socket?.user?.email || socket?.request?.user?.email || socket?.handshake?.auth?.email || "unknown@user";
|
||||||
|
|
||||||
|
const jobid = socket?.JobData?.id || txnDetails?.jobid || null;
|
||||||
|
|
||||||
|
// Main logging entry (to CloudWatch / file)
|
||||||
|
logger.log("rr-log-event", levelUpper, userEmail, jobid, {
|
||||||
wsmessage: message,
|
wsmessage: message,
|
||||||
txnDetails
|
txnDetails
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
// Best-effort: never throw from logging
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Realtime push to the UI if we're in a socket context
|
// Emit to live Socket.IO client if available
|
||||||
try {
|
if (typeof socket.emit === "function") {
|
||||||
if (typeof socket?.emit === "function") {
|
socket.emit("rr-log-event", {
|
||||||
socket.emit("rr-log-event", { level, message, txnDetails });
|
level: levelUpper,
|
||||||
|
message,
|
||||||
|
txnDetails,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Best-effort: never throw from logging
|
// As a fallback, log directly to console
|
||||||
|
console.error("RRLogger internal error:", err);
|
||||||
|
console.error("Original message:", message, txnDetails);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = CreateRRLogEvent;
|
module.exports = RRLogger;
|
||||||
|
|||||||
@@ -1,88 +1,143 @@
|
|||||||
// -----------------------------------------------------------------------------
|
/**
|
||||||
// Reynolds & Reynolds (RR) lookup helpers.
|
* @file rr-lookup.js
|
||||||
// Uses MakeRRCall + RRActions from rr-helpers, and shared response validation
|
* @description Reynolds & Reynolds lookup operations
|
||||||
// from rr-error.
|
* (Combined Search, Get Advisors, Get Parts) via SOAP/XML templates.
|
||||||
//
|
*/
|
||||||
// What’s still missing / to confirm against the Rome/RR PDFs:
|
|
||||||
// - Final query param names and value formats for the “combined search”
|
|
||||||
// (customer + vehicle), advisors directory, and parts lookup.
|
|
||||||
// - Any RR-required headers (dealer/site/location ids) — add in rr-helpers
|
|
||||||
// via the MakeRRCall default headers if needed.
|
|
||||||
// - Final success envelope checks in assertRrOk.
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
const { MakeRRCall, RRActions, getDealerConfig } = require("./rr-helpers");
|
||||||
const { assertRrOk } = require("./rr-error");
|
const { assertRrOkXml, extractRrResponseData } = require("./rr-error");
|
||||||
|
const { mapCombinedSearchVars, mapGetAdvisorsVars, mapGetPartsVars } = require("./rr-mappers");
|
||||||
|
const RRLogger = require("./rr-logger");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RR Combined Search (Customer + Vehicle).
|
* Combined Search
|
||||||
|
* Maps to "Search Customer Service Vehicle Combined" spec (Rome)
|
||||||
*
|
*
|
||||||
* @param {Object} deps
|
* @param {object} options
|
||||||
* @param {Socket|ExpressRequest} deps.socket
|
* @param {object} options.socket - Socket or Express req (used for auth + bodyshopId)
|
||||||
* @param {Object} deps.redisHelpers
|
* @param {object} options.redisHelpers - (unused, kept for parity)
|
||||||
* @param {string|number} deps.jobid - for correlation/logging only
|
* @param {string} options.jobid - Job reference for correlation
|
||||||
* @param {Array<[string,string]>} [deps.params=[]]
|
* @param {Array<[string, string]>} [options.params] - e.g. [["VIN","1HG..."],["LastName","DOE"]]
|
||||||
* Example: [["vin", "1HGBH41JXMN109186"], ["lastName","DOE"]]
|
|
||||||
* @returns {Promise<any>} RR response (envelope TBD)
|
|
||||||
*/
|
*/
|
||||||
async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) {
|
async function RrCombinedSearch({ socket, redisHelpers, jobid, params = [] }) {
|
||||||
const data = await MakeRRCall({
|
try {
|
||||||
...RRActions.CombinedSearch, // GET /search/v1/customer-vehicle
|
RRLogger(socket, "info", "Starting RR Combined Search", { jobid, params });
|
||||||
requestSearchParams: params,
|
|
||||||
type: "get",
|
|
||||||
redisHelpers,
|
|
||||||
socket,
|
|
||||||
jobid
|
|
||||||
});
|
|
||||||
|
|
||||||
// allowEmpty=true because searches may legitimately return 0 rows
|
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
||||||
return assertRrOk(data, { apiName: "RR Combined Search", allowEmpty: true });
|
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||||
|
|
||||||
|
// Build Mustache variables for server/rr/xml-templates/CombinedSearch.xml
|
||||||
|
const variables = mapCombinedSearchVars({ params, dealerConfig });
|
||||||
|
|
||||||
|
const xml = await MakeRRCall({
|
||||||
|
action: RRActions.CombinedSearch,
|
||||||
|
body: { template: "CombinedSearch", data: variables },
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate + normalize
|
||||||
|
const ok = assertRrOkXml(xml, { apiName: "RR Combined Search", allowEmpty: true });
|
||||||
|
const normalized = extractRrResponseData(ok, { action: "CombinedSearch" });
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", "RR Combined Search complete", {
|
||||||
|
jobid,
|
||||||
|
count: Array.isArray(normalized) ? normalized.length : 0
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR Combined Search failed: ${error.message}`, { jobid });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RR Get Advisors.
|
* Get Advisors
|
||||||
|
* Maps to "Get Advisors Specification" (Rome)
|
||||||
*
|
*
|
||||||
* @param {Object} deps
|
* @param {object} options
|
||||||
* @param {Socket|ExpressRequest} deps.socket
|
* @param {object} options.socket
|
||||||
* @param {Object} deps.redisHelpers
|
* @param {object} options.redisHelpers
|
||||||
* @param {string|number} deps.jobid
|
* @param {string} options.jobid
|
||||||
* @param {Array<[string,string]>} [deps.params=[]]
|
* @param {Array<[string, string]>} [options.params]
|
||||||
* Example: [["active","true"]]
|
|
||||||
* @returns {Promise<any>} RR response (envelope TBD)
|
|
||||||
*/
|
*/
|
||||||
async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) {
|
async function RrGetAdvisors({ socket, redisHelpers, jobid, params = [] }) {
|
||||||
const data = await MakeRRCall({
|
try {
|
||||||
...RRActions.GetAdvisors, // GET /advisors/v1
|
RRLogger(socket, "info", "Starting RR Get Advisors", { jobid, params });
|
||||||
requestSearchParams: params,
|
|
||||||
type: "get",
|
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
||||||
redisHelpers,
|
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||||
socket,
|
|
||||||
jobid
|
// Build Mustache variables for server/rr/xml-templates/GetAdvisors.xml
|
||||||
});
|
const variables = mapGetAdvisorsVars({ params, dealerConfig });
|
||||||
return assertRrOk(data, { apiName: "RR Get Advisors", allowEmpty: true });
|
|
||||||
|
const xml = await MakeRRCall({
|
||||||
|
action: RRActions.GetAdvisors,
|
||||||
|
body: { template: "GetAdvisors", data: variables },
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = assertRrOkXml(xml, { apiName: "RR Get Advisors", allowEmpty: true });
|
||||||
|
const normalized = extractRrResponseData(ok, { action: "GetAdvisors" });
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", "RR Get Advisors complete", {
|
||||||
|
jobid,
|
||||||
|
count: Array.isArray(normalized) ? normalized.length : 0
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR Get Advisors failed: ${error.message}`, { jobid });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RR Get Parts.
|
* Get Parts
|
||||||
|
* Maps to "Get Part Specification" (Rome)
|
||||||
*
|
*
|
||||||
* @param {Object} deps
|
* @param {object} options
|
||||||
* @param {Socket|ExpressRequest} deps.socket
|
* @param {object} options.socket
|
||||||
* @param {Object} deps.redisHelpers
|
* @param {object} options.redisHelpers
|
||||||
* @param {string|number} deps.jobid
|
* @param {string} options.jobid
|
||||||
* @param {Array<[string,string]>} [deps.params=[]]
|
* @param {Array<[string, string]>} [options.params]
|
||||||
* Example: [["sku","ABC123"], ["page","1"], ["pageSize","50"]]
|
|
||||||
* @returns {Promise<any>} RR response (envelope TBD)
|
|
||||||
*/
|
*/
|
||||||
async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) {
|
async function RrGetParts({ socket, redisHelpers, jobid, params = [] }) {
|
||||||
const data = await MakeRRCall({
|
try {
|
||||||
...RRActions.GetParts, // GET /parts/v1
|
RRLogger(socket, "info", "Starting RR Get Parts", { jobid, params });
|
||||||
requestSearchParams: params,
|
|
||||||
type: "get",
|
const bodyshopId = socket?.bodyshopId || socket?.user?.bodyshopid;
|
||||||
redisHelpers,
|
const dealerConfig = bodyshopId ? await getDealerConfig(bodyshopId) : {};
|
||||||
socket,
|
|
||||||
jobid
|
// Build Mustache variables for server/rr/xml-templates/GetParts.xml
|
||||||
});
|
const variables = mapGetPartsVars({ params, dealerConfig });
|
||||||
return assertRrOk(data, { apiName: "RR Get Parts", allowEmpty: true });
|
|
||||||
|
const xml = await MakeRRCall({
|
||||||
|
action: RRActions.GetParts,
|
||||||
|
body: { template: "GetParts", data: variables },
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid
|
||||||
|
});
|
||||||
|
|
||||||
|
const ok = assertRrOkXml(xml, { apiName: "RR Get Parts", allowEmpty: true });
|
||||||
|
const normalized = extractRrResponseData(ok, { action: "GetParts" });
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", "RR Get Parts complete", {
|
||||||
|
jobid,
|
||||||
|
count: Array.isArray(normalized) ? normalized.length : 0
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR Get Parts failed: ${error.message}`, { jobid });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { RrCombinedSearch, RrGetAdvisors, RrGetParts };
|
module.exports = {
|
||||||
|
RrCombinedSearch,
|
||||||
|
RrGetAdvisors,
|
||||||
|
RrGetParts
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,333 +1,424 @@
|
|||||||
|
// server/rr/rr-mappers.js
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Centralized mapping & normalization for Reynolds & Reynolds (RR)
|
// Centralized mapping for Reynolds & Reynolds (RR) XML templates.
|
||||||
|
// These functions take our domain objects (JobData, txEnvelope, current/patch)
|
||||||
|
// and produce the Mustache variable objects expected by the RR XML templates in
|
||||||
|
// /server/rr/xml-templates.
|
||||||
//
|
//
|
||||||
// This is scaffolding aligned to the Rome RR PDFs you provided:
|
// NOTE: This is still scaffolding. Where “TODO (spec)” appears, fill in the
|
||||||
|
// exact RR field semantics (type restrictions, enums, required/optional) based
|
||||||
|
// on the Rome RR PDFs you shared.
|
||||||
//
|
//
|
||||||
// - Rome Customer Insert Specification 1.2.pdf
|
// Templates these map into (variable names must match):
|
||||||
// - Rome Customer Update Specification 1.2.pdf
|
// - InsertCustomer.xml: <rr:CustomerInsertRq/>
|
||||||
// - Rome Insert Service Vehicle Interface Specification.pdf
|
// - UpdateCustomer.xml: <rr:CustomerUpdateRq/>
|
||||||
// - Rome Create Body Shop Management Repair Order Interface Specification.pdf
|
// - InsertServiceVehicle.xml: <rr:ServiceVehicleAddRq/>
|
||||||
// - Rome Update Body Shop Management Repair Order Interface Specification.pdf
|
// - CreateRepairOrder.xml: <rr:RepairOrderInsertRq/>
|
||||||
// - Rome Get Advisors Specification.pdf
|
// - UpdateRepairOrder.xml: <rr:RepairOrderChgRq/>
|
||||||
// - Rome Get Part Specification.pdf
|
|
||||||
// - Rome Search Customer Service Vehicle Combined Specification.pdf
|
|
||||||
//
|
//
|
||||||
// Replace all TODO:RR with exact element/attribute names and enumerations from
|
// All map* functions below return a plain object shaped for Mustache rendering.
|
||||||
// the PDFs above. The shapes here are intentionally close to other providers
|
|
||||||
// so you can reuse upstream plumbing without surprises.
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
const _ = require("lodash");
|
const _ = require("lodash");
|
||||||
|
|
||||||
// Keep this consistent with other providers
|
// Keep this consistent with other providers (sanitize strings for XML)
|
||||||
const replaceSpecialRegex = /[^a-zA-Z0-9 .,\n #]+/g;
|
const REPLACE_SPECIAL = /[^a-zA-Z0-9 .,\n#\-()/]+/g;
|
||||||
|
|
||||||
// ---------- Generic helpers --------------------------------------------------
|
function sanitize(v) {
|
||||||
|
if (v === null || v === undefined) return null;
|
||||||
function sanitize(value) {
|
return String(v).replace(REPLACE_SPECIAL, "").trim();
|
||||||
if (value === null || value === undefined) return value;
|
|
||||||
return String(value).replace(replaceSpecialRegex, "").trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function asStringOrNull(value) {
|
function upper(v) {
|
||||||
const s = sanitize(value);
|
const s = sanitize(v);
|
||||||
return s && s.length > 0 ? s : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toUpperOrNull(value) {
|
|
||||||
const s = asStringOrNull(value);
|
|
||||||
return s ? s.toUpperCase() : null;
|
return s ? s.toUpperCase() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function asNumberOrNull(v) {
|
||||||
* Normalize postal/zip minimally; keep simple and provider-agnostic for now.
|
if (v === null || v === undefined || v === "") return null;
|
||||||
* TODO:RR — If RR enforces specific postal formatting by country, implement it here.
|
const n = Number(v);
|
||||||
*/
|
return Number.isFinite(n) ? n : null;
|
||||||
function normalizePostal(raw) {
|
|
||||||
if (!raw) return null;
|
|
||||||
return String(raw).trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePostal(raw) {
|
||||||
|
if (!raw) return null;
|
||||||
|
const s = String(raw).toUpperCase().replace(/\s+/g, "");
|
||||||
|
// If Canadian format (A1A1A1), keep as-is. Otherwise return raw sanitized.
|
||||||
|
return s.length === 6 ? `${s.slice(0, 3)} ${s.slice(3)}` : sanitize(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose the dealer section used by every template.
|
||||||
|
* We prefer dealer-level rr_configuration first; fallback to env.
|
||||||
|
*/
|
||||||
|
function buildDealerVars(dealerCfg = {}) {
|
||||||
|
return {
|
||||||
|
DealerCode: dealerCfg.dealerCode || process.env.RR_DEALER_CODE || null,
|
||||||
|
DealerName: dealerCfg.dealerName || process.env.RR_DEALER_NAME || null,
|
||||||
|
DealerNumber: dealerCfg.dealerNumber || process.env.RR_DEALER_NUMBER || null,
|
||||||
|
StoreNumber: dealerCfg.storeNumber || process.env.RR_STORE_NUMBER || null,
|
||||||
|
BranchNumber: dealerCfg.branchNumber || process.env.RR_BRANCH_NUMBER || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------- Phones/Emails ------------------------------- */
|
||||||
|
|
||||||
function mapPhones({ ph1, ph2, mobile }) {
|
function mapPhones({ ph1, ph2, mobile }) {
|
||||||
// TODO:RR — Replace "HOME|WORK|MOBILE" with RR's phone type codes + any flags (preferred, sms ok).
|
// TODO (spec): adjust PhoneType enumerations if RR requires strict codes.
|
||||||
const out = [];
|
const out = [];
|
||||||
if (ph1) out.push({ number: sanitize(ph1), type: "HOME" });
|
if (ph1) out.push({ PhoneNumber: sanitize(ph1), PhoneType: "HOME" });
|
||||||
if (ph2) out.push({ number: sanitize(ph2), type: "WORK" });
|
if (ph2) out.push({ PhoneNumber: sanitize(ph2), PhoneType: "WORK" });
|
||||||
if (mobile) out.push({ number: sanitize(mobile), type: "MOBILE" });
|
if (mobile) out.push({ PhoneNumber: sanitize(mobile), PhoneType: "MOBILE" });
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapEmails({ email }) {
|
function mapEmails({ email }) {
|
||||||
// TODO:RR — If RR supports multiple with flags, expand (preferred, statement, etc.).
|
|
||||||
if (!email) return [];
|
if (!email) return [];
|
||||||
return [{ address: sanitize(email), type: "PERSONAL" }];
|
// TODO (spec): include EmailType (e.g., PERSONAL/WORK) if RR mandates it.
|
||||||
|
return [{ EmailAddress: sanitize(email), EmailType: "PERSONAL" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Address/Contact from Rome JobData --------------------------------
|
/* -------------------------------- Addresses -------------------------------- */
|
||||||
|
|
||||||
function mapPostalAddressFromJob(job) {
|
function mapPostalAddressFromJob(job) {
|
||||||
// Rome job-level owner fields (aligned with other providers)
|
return [
|
||||||
// TODO:RR — Confirm exact element names (e.g., AddressLine vs Street1, State vs Province).
|
{
|
||||||
return {
|
AddressLine1: sanitize(job.ownr_addr1),
|
||||||
addressLine1: asStringOrNull(job.ownr_addr1),
|
AddressLine2: sanitize(job.ownr_addr2),
|
||||||
addressLine2: asStringOrNull(job.ownr_addr2),
|
City: upper(job.ownr_city),
|
||||||
city: asStringOrNull(job.ownr_city),
|
State: upper(job.ownr_st || job.ownr_state),
|
||||||
state: asStringOrNull(job.ownr_st || job.ownr_state),
|
PostalCode: normalizePostal(job.ownr_zip),
|
||||||
province: asStringOrNull(job.ownr_province), // keep both for CA use-cases if distinct in RR
|
Country: upper(job.ownr_ctry) || "USA"
|
||||||
postalCode: normalizePostal(job.ownr_zip),
|
|
||||||
country: asStringOrNull(job.ownr_ctry) || "USA"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapPhonesFromJob(job) {
|
|
||||||
return mapPhones({
|
|
||||||
ph1: job.ownr_ph1,
|
|
||||||
ph2: job.ownr_ph2,
|
|
||||||
mobile: job.ownr_mobile
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapEmailsFromJob(job) {
|
|
||||||
return mapEmails({ email: job.ownr_ea });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- Customer mappers --------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customer Insert
|
|
||||||
* Matches call-site: const body = mapCustomerInsert(JobData);
|
|
||||||
*
|
|
||||||
* TODO:RR — Replace envelope and field names with exact RR schema:
|
|
||||||
* e.g., CustomerInsertRq.Customer (Organization vs Person), name blocks, ids, codes, etc.
|
|
||||||
*/
|
|
||||||
function mapCustomerInsert(job) {
|
|
||||||
const isCompany = Boolean(job?.ownr_co_nm && job.ownr_co_nm.trim() !== "");
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Example envelope — rename to match the PDF (e.g., "CustomerInsertRq")
|
|
||||||
CustomerInsertRq: {
|
|
||||||
// High-level type — confirm the exact enum RR expects.
|
|
||||||
customerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
|
|
||||||
|
|
||||||
// Name block — ensure RR's exact element names and casing.
|
|
||||||
customerName: {
|
|
||||||
companyName: isCompany ? toUpperOrNull(job.ownr_co_nm) : null,
|
|
||||||
firstName: isCompany ? null : toUpperOrNull(job.ownr_fn),
|
|
||||||
lastName: isCompany ? null : toUpperOrNull(job.ownr_ln)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mailing address
|
|
||||||
postalAddress: mapPostalAddressFromJob(job),
|
|
||||||
|
|
||||||
// Contacts
|
|
||||||
contactMethods: {
|
|
||||||
phones: mapPhonesFromJob(job),
|
|
||||||
emailAddresses: mapEmailsFromJob(job)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO:RR — Common optional fields: tax/resale codes, pricing flags, AR terms, source codes, etc.
|
|
||||||
// taxCode: null,
|
|
||||||
// termsCode: null,
|
|
||||||
// marketingOptIn: null,
|
|
||||||
// dealerSpecificFields: []
|
|
||||||
}
|
}
|
||||||
|
].filter((addr) => Object.values(addr).some(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------- Customer -------------------------------- */
|
||||||
|
|
||||||
|
function mapCustomerInsert(job, dealerCfg = {}) {
|
||||||
|
const dealer = buildDealerVars(dealerCfg);
|
||||||
|
const isCompany = Boolean(job?.ownr_co_nm && String(job.ownr_co_nm).trim() !== "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...dealer,
|
||||||
|
// Envelope metadata (optional)
|
||||||
|
RequestId: job?.id || null,
|
||||||
|
Environment: process.env.NODE_ENV || "development",
|
||||||
|
|
||||||
|
// Customer node (see InsertCustomer.xml)
|
||||||
|
CustomerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
|
||||||
|
CompanyName: isCompany ? upper(job.ownr_co_nm) : null,
|
||||||
|
FirstName: !isCompany ? upper(job.ownr_fn) : null,
|
||||||
|
LastName: !isCompany ? upper(job.ownr_ln) : null,
|
||||||
|
ActiveFlag: "Y",
|
||||||
|
|
||||||
|
Addresses: mapPostalAddressFromJob(job),
|
||||||
|
Phones: mapPhones({ ph1: job.ownr_ph1, ph2: job.ownr_ph2, mobile: job.ownr_mobile }),
|
||||||
|
Emails: mapEmails({ email: job.ownr_ea }),
|
||||||
|
|
||||||
|
// Optional blocks (keep null unless you truly have values)
|
||||||
|
DriverLicense: null, // { LicenseNumber, LicenseState, ExpirationDate }
|
||||||
|
Insurance: null, // { CompanyName, PolicyNumber, ExpirationDate }
|
||||||
|
Notes: null // { Note }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function mapCustomerUpdate(existingCustomer, patch = {}, dealerCfg = {}) {
|
||||||
* Customer Update
|
const dealer = buildDealerVars(dealerCfg);
|
||||||
* Matches call-site: const body = mapCustomerUpdate(existingCustomer, patch);
|
// We merge and normalize so callers can pass minimal deltas
|
||||||
*
|
|
||||||
* - existingCustomer: RR's current representation (from Read/Query)
|
|
||||||
* - patch: a thin delta from UI/Job selection
|
|
||||||
*
|
|
||||||
* TODO:RR — Swap envelope/fields for RR's specific Update schema.
|
|
||||||
*/
|
|
||||||
function mapCustomerUpdate(existingCustomer, patch = {}) {
|
|
||||||
const merged = _.merge({}, existingCustomer || {}, patch || {});
|
const merged = _.merge({}, existingCustomer || {}, patch || {});
|
||||||
const id = merged?.customerId || merged?.id || merged?.CustomerId || merged?.customer?.id || null;
|
const id =
|
||||||
|
merged?.CustomerId ||
|
||||||
|
merged?.customerId ||
|
||||||
|
merged?.id ||
|
||||||
|
merged?.customer?.id ||
|
||||||
|
patch?.CustomerId ||
|
||||||
|
patch?.customerId ||
|
||||||
|
null;
|
||||||
|
|
||||||
const isCompany = Boolean(merged?.customerName?.companyName) || Boolean(merged?.companyName);
|
// Derive company vs individual
|
||||||
|
const isCompany = Boolean(merged?.CompanyName || merged?.customerName?.companyName);
|
||||||
|
|
||||||
const normalizedName = {
|
const nameBlock = {
|
||||||
companyName: asStringOrNull(merged?.customerName?.companyName) || asStringOrNull(merged?.companyName) || null,
|
CompanyName: isCompany ? upper(merged?.CompanyName || merged?.customerName?.companyName) : null,
|
||||||
firstName: isCompany
|
FirstName: !isCompany ? upper(merged?.FirstName || merged?.customerName?.firstName) : null,
|
||||||
? null
|
LastName: !isCompany ? upper(merged?.LastName || merged?.customerName?.lastName) : null
|
||||||
: asStringOrNull(merged?.customerName?.firstName) || asStringOrNull(merged?.firstName) || null,
|
|
||||||
lastName: isCompany
|
|
||||||
? null
|
|
||||||
: asStringOrNull(merged?.customerName?.lastName) || asStringOrNull(merged?.lastName) || null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizedAddress = {
|
// Addresses
|
||||||
addressLine1: asStringOrNull(merged?.postalAddress?.addressLine1) || asStringOrNull(merged?.addressLine1) || null,
|
const addr =
|
||||||
addressLine2: asStringOrNull(merged?.postalAddress?.addressLine2) || asStringOrNull(merged?.addressLine2) || null,
|
merged?.Addresses ||
|
||||||
city: asStringOrNull(merged?.postalAddress?.city) || asStringOrNull(merged?.city) || null,
|
merged?.postalAddress ||
|
||||||
state:
|
(merged?.addressLine1 || merged?.addressLine2 || merged?.city
|
||||||
asStringOrNull(merged?.postalAddress?.state) ||
|
? [
|
||||||
asStringOrNull(merged?.state) ||
|
{
|
||||||
asStringOrNull(merged?.stateOrProvince) ||
|
AddressLine1: sanitize(merged?.addressLine1),
|
||||||
asStringOrNull(merged?.province) ||
|
AddressLine2: sanitize(merged?.addressLine2),
|
||||||
null,
|
City: upper(merged?.city),
|
||||||
province: asStringOrNull(merged?.postalAddress?.province) || asStringOrNull(merged?.province) || null,
|
State: upper(merged?.state || merged?.province),
|
||||||
postalCode: normalizePostal(merged?.postalAddress?.postalCode || merged?.postalCode),
|
PostalCode: normalizePostal(merged?.postalCode),
|
||||||
country: asStringOrNull(merged?.postalAddress?.country) || asStringOrNull(merged?.country) || "USA"
|
Country: upper(merged?.country) || "USA"
|
||||||
};
|
}
|
||||||
|
]
|
||||||
|
: null);
|
||||||
|
|
||||||
// Contacts (reuse existing unless patch supplied a new structure upstream)
|
// Phones & Emails
|
||||||
const normalizedPhones = merged?.contactMethods?.phones || merged?.phones || [];
|
const phones = merged?.Phones || merged?.contactMethods?.phones || [];
|
||||||
|
const emails = merged?.Emails || merged?.contactMethods?.emailAddresses || [];
|
||||||
const normalizedEmails = merged?.contactMethods?.emailAddresses || merged?.emailAddresses || [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Example envelope — rename to match the PDF (e.g., "CustomerUpdateRq")
|
...dealer,
|
||||||
CustomerUpdateRq: {
|
RequestId: merged?.RequestId || null,
|
||||||
customerId: id,
|
Environment: process.env.NODE_ENV || "development",
|
||||||
customerType: normalizedName.companyName ? "ORGANIZATION" : "INDIVIDUAL",
|
|
||||||
customerName: normalizedName,
|
CustomerId: id,
|
||||||
postalAddress: normalizedAddress,
|
CustomerType: isCompany ? "ORGANIZATION" : "INDIVIDUAL",
|
||||||
contactMethods: {
|
...nameBlock,
|
||||||
phones: normalizedPhones,
|
ActiveFlag: merged?.ActiveFlag || "Y",
|
||||||
emailAddresses: normalizedEmails
|
|
||||||
}
|
Addresses: addr,
|
||||||
// TODO:RR — include fields that RR requires for update (version, hash, lastUpdatedTs, etc.)
|
Phones: phones.map((p) => ({ PhoneNumber: sanitize(p.PhoneNumber || p.number), PhoneType: p.PhoneType || p.type })),
|
||||||
}
|
Emails: emails.map((e) => ({
|
||||||
|
EmailAddress: sanitize(e.EmailAddress || e.address),
|
||||||
|
EmailType: e.EmailType || e.type || "PERSONAL"
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Optional
|
||||||
|
DriverLicense: merged?.DriverLicense || null,
|
||||||
|
Insurance: merged?.Insurance || null,
|
||||||
|
Notes: merged?.Notes || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Vehicle mappers ---------------------------------------------------
|
/* --------------------------------- Vehicle --------------------------------- */
|
||||||
|
|
||||||
|
function mapVehicleInsertFromJob(job, dealerCfg = {}, opts = {}) {
|
||||||
|
// opts: { customerId }
|
||||||
|
const dealer = buildDealerVars(dealerCfg);
|
||||||
|
|
||||||
/**
|
|
||||||
* Vehicle Insert from JobData
|
|
||||||
* Called (or call-able) by InsertVehicle.
|
|
||||||
*
|
|
||||||
* TODO:RR — Replace envelope/field names with the exact RR vehicle schema.
|
|
||||||
*/
|
|
||||||
function mapVehicleInsertFromJob(job, txEnvelope = {}) {
|
|
||||||
return {
|
return {
|
||||||
ServiceVehicleInsertRq: {
|
...dealer,
|
||||||
vin: asStringOrNull(job.v_vin),
|
RequestId: job?.id || null,
|
||||||
// Year/make/model — validate source fields vs RR required fields
|
Environment: process.env.NODE_ENV || "development",
|
||||||
year: job.v_model_yr || null,
|
|
||||||
make: toUpperOrNull(txEnvelope.dms_make || job.v_make),
|
CustomerId: opts?.customerId || null,
|
||||||
model: toUpperOrNull(txEnvelope.dms_model || job.v_model),
|
|
||||||
// Mileage/odometer — confirm units/element names
|
VIN: upper(job?.v_vin),
|
||||||
odometer: txEnvelope.kmout || txEnvelope.miout || null,
|
Year: asNumberOrNull(job?.v_model_yr),
|
||||||
// Plate — uppercase and sanitize
|
Make: upper(job?.v_make),
|
||||||
licensePlate: job.plate_no ? toUpperOrNull(job.plate_no) : null
|
Model: upper(job?.v_model),
|
||||||
// TODO:RR — owner/customer link, color, trim, fuel, DRIVETRAIN, etc.
|
Trim: upper(job?.v_trim),
|
||||||
}
|
BodyStyle: upper(job?.v_body),
|
||||||
|
Transmission: upper(job?.v_transmission),
|
||||||
|
Engine: upper(job?.v_engine),
|
||||||
|
FuelType: upper(job?.v_fuel),
|
||||||
|
Color: upper(job?.v_color),
|
||||||
|
Odometer: asNumberOrNull(job?.odometer_out || job?.kmout),
|
||||||
|
LicensePlate: upper(job?.plate_no),
|
||||||
|
LicenseState: upper(job?.plate_state),
|
||||||
|
|
||||||
|
Ownership: null,
|
||||||
|
Insurance: null,
|
||||||
|
VehicleNotes: null,
|
||||||
|
Warranty: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Repair Order mappers ---------------------------------------------
|
/* ------------------------------- Repair Orders ------------------------------ */
|
||||||
|
|
||||||
|
function mapRepairOrderAddFromJob(job, txEnvelope = {}, dealerCfg = {}) {
|
||||||
|
const dealer = buildDealerVars(dealerCfg);
|
||||||
|
|
||||||
|
const customerVars = {
|
||||||
|
CustomerId: job?.customer?.id || txEnvelope?.customerId || null,
|
||||||
|
CustomerName:
|
||||||
|
upper(job?.ownr_co_nm) || [upper(job?.ownr_fn), upper(job?.ownr_ln)].filter(Boolean).join(" ").trim() || null,
|
||||||
|
PhoneNumber: sanitize(job?.ownr_ph1 || job?.ownr_mobile || job?.ownr_ph2),
|
||||||
|
EmailAddress: sanitize(job?.ownr_ea)
|
||||||
|
};
|
||||||
|
|
||||||
|
const vehicleVars = {
|
||||||
|
VIN: upper(job?.v_vin),
|
||||||
|
LicensePlate: upper(job?.plate_no),
|
||||||
|
Year: asNumberOrNull(job?.v_model_yr),
|
||||||
|
Make: upper(job?.v_make),
|
||||||
|
Model: upper(job?.v_model),
|
||||||
|
Odometer: asNumberOrNull(job?.odometer_out || job?.kmout),
|
||||||
|
Color: upper(job?.v_color)
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Create Repair Order
|
|
||||||
* Matches call-site: mapRepairOrderCreate({ JobData, txEnvelope })
|
|
||||||
*
|
|
||||||
* TODO:RR — Use the exact request envelope/fields for Create RO from the PDF:
|
|
||||||
* Header (customer/vehicle/ro-no/dates), lines/labors/parts/taxes, totals.
|
|
||||||
*/
|
|
||||||
function mapRepairOrderCreate({ JobData, txEnvelope }) {
|
|
||||||
return {
|
return {
|
||||||
RepairOrderCreateRq: {
|
...dealer,
|
||||||
// Header
|
RequestId: job?.id || null,
|
||||||
referenceNumber: asStringOrNull(JobData.ro_number),
|
Environment: process.env.NODE_ENV || "development",
|
||||||
customerId: JobData?.customer?.id || null, // supply from previous step or selection
|
|
||||||
vehicleId: JobData?.vehicle?.id || null, // supply from previous step
|
|
||||||
openedAt: JobData?.actual_in || null, // confirm expected datetime format
|
|
||||||
promisedAt: JobData?.promise_date || null,
|
|
||||||
advisorId: txEnvelope?.advisorId || null,
|
|
||||||
|
|
||||||
// Lines (placeholder)
|
RepairOrderNumber: sanitize(job?.ro_number) || sanitize(txEnvelope?.reference) || null,
|
||||||
lines: Array.isArray(JobData?.joblines) ? JobData.joblines.map(mapJobLineToRRLine) : [],
|
OpenDate: txEnvelope?.openedAt || job?.actual_in || null,
|
||||||
|
PromisedDate: txEnvelope?.promisedAt || job?.promise_date || null,
|
||||||
|
CloseDate: txEnvelope?.closedAt || job?.invoice_date || null,
|
||||||
|
ServiceAdvisorId: txEnvelope?.advisorId || job?.service_advisor_id || null,
|
||||||
|
TechnicianId: txEnvelope?.technicianId || job?.technician_id || null,
|
||||||
|
ROType: txEnvelope?.roType || "CUSTOMER_PAY", // TODO (spec): map from our job type(s)
|
||||||
|
Status: txEnvelope?.status || "OPEN",
|
||||||
|
|
||||||
// Taxes (placeholder)
|
CustomerId: customerVars.CustomerId,
|
||||||
taxes: mapTaxes(JobData),
|
CustomerName: customerVars.CustomerName,
|
||||||
|
PhoneNumber: customerVars.PhoneNumber,
|
||||||
|
EmailAddress: customerVars.EmailAddress,
|
||||||
|
|
||||||
// Payments (placeholder)
|
VIN: vehicleVars.VIN,
|
||||||
payments: mapPayments(txEnvelope)
|
LicensePlate: vehicleVars.LicensePlate,
|
||||||
|
Year: vehicleVars.Year,
|
||||||
|
Make: vehicleVars.Make,
|
||||||
|
Model: vehicleVars.Model,
|
||||||
|
Odometer: vehicleVars.Odometer,
|
||||||
|
Color: vehicleVars.Color,
|
||||||
|
|
||||||
// TODO:RR — add required flags, shop supplies, labor matrix, discounts, etc.
|
JobLines: (job?.joblines || txEnvelope?.lines || []).map((ln, idx) => mapJobLineToRRLine(ln, idx + 1)),
|
||||||
}
|
|
||||||
|
Totals: txEnvelope?.totals
|
||||||
|
? {
|
||||||
|
LaborTotal: asNumberOrNull(txEnvelope.totals.labor),
|
||||||
|
PartsTotal: asNumberOrNull(txEnvelope.totals.parts),
|
||||||
|
MiscTotal: asNumberOrNull(txEnvelope.totals.misc),
|
||||||
|
TaxTotal: asNumberOrNull(txEnvelope.totals.tax),
|
||||||
|
GrandTotal: asNumberOrNull(txEnvelope.totals.total)
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
|
||||||
|
Insurance: txEnvelope?.insurance
|
||||||
|
? {
|
||||||
|
CompanyName: upper(txEnvelope.insurance.company),
|
||||||
|
ClaimNumber: sanitize(txEnvelope.insurance.claim),
|
||||||
|
AdjusterName: upper(txEnvelope.insurance.adjuster),
|
||||||
|
AdjusterPhone: sanitize(txEnvelope.insurance.phone)
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
|
||||||
|
Notes: txEnvelope?.story ? { Note: sanitize(txEnvelope.story) } : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function mapRepairOrderChangeFromJob(current, delta = {}, dealerCfg = {}) {
|
||||||
* Update Repair Order
|
// current: existing RO (our cached shape)
|
||||||
* Matches call-site: mapRepairOrderUpdate({ JobData, txEnvelope })
|
// delta: patch object describing header fields and line changes
|
||||||
*
|
const dealer = buildDealerVars(dealerCfg);
|
||||||
* TODO:RR — RR may want delta format (change set) vs full replace.
|
|
||||||
* Add versioning/concurrency tokens if specified in the PDF.
|
|
||||||
*/
|
|
||||||
function mapRepairOrderUpdate({ JobData, txEnvelope }) {
|
|
||||||
return {
|
|
||||||
RepairOrderUpdateRq: {
|
|
||||||
repairOrderId: JobData?.id || txEnvelope?.repairOrderId || null,
|
|
||||||
referenceNumber: asStringOrNull(JobData?.ro_number),
|
|
||||||
|
|
||||||
// Example: only pass changed lines (you may need your diff before mapping)
|
const added = (delta.addedLines || []).map((ln, i) =>
|
||||||
// For scaffolding, we pass what we have; replace with proper deltas later.
|
mapJobLineToRRLine(ln, ln.Sequence || ln.seq || i + 1, { includePayType: true })
|
||||||
lines: Array.isArray(JobData?.joblines) ? JobData.joblines.map(mapJobLineToRRLine) : [],
|
);
|
||||||
|
const updated = (delta.updatedLines || []).map((ln) => ({
|
||||||
taxes: mapTaxes(JobData),
|
...mapJobLineToRRLine(ln, ln.Sequence || ln.seq, { includePayType: true }),
|
||||||
payments: mapPayments(txEnvelope)
|
ChangeType: ln.ChangeType || ln.change || null,
|
||||||
|
LineId: ln.LineId || null
|
||||||
// TODO:RR — include RO status transitions, close/invoice flags, etc.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----- Line/Tax/Payment helpers (placeholders) ----------------------------- */
|
|
||||||
|
|
||||||
function mapJobLineToRRLine(line) {
|
|
||||||
// TODO:RR — Replace with RR RO line schema (labor/part/misc line types, op-code, flags).
|
|
||||||
return {
|
|
||||||
lineType: line?.type || "LABOR", // e.g., LABOR | PART | MISC
|
|
||||||
sequence: line?.sequence || null,
|
|
||||||
opCode: line?.opCode || line?.opcode || null,
|
|
||||||
description: asStringOrNull(line?.description || line?.descr),
|
|
||||||
quantity: line?.part_qty || line?.qty || 1,
|
|
||||||
unitPrice: line?.price || line?.unitPrice || null,
|
|
||||||
extendedAmount: line?.ext || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapTaxes(job) {
|
|
||||||
// TODO:RR — Implement per RR tax structure (rates by jurisdiction, taxable flags, rounding rules).
|
|
||||||
// Return empty array as scaffolding.
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapPayments(txEnvelope = {}) {
|
|
||||||
// TODO:RR — Implement per RR payment shape (payer types, amounts, reference ids)
|
|
||||||
// For Fortellis/CDK parity, txEnvelope.payers often exists; adapt to RR fields.
|
|
||||||
if (!Array.isArray(txEnvelope?.payers)) return [];
|
|
||||||
return txEnvelope.payers.map((p) => ({
|
|
||||||
payerType: p.type || "INSURER", // e.g., CUSTOMER | INSURER | WARRANTY
|
|
||||||
reference: asStringOrNull(p.controlnumber || p.ref),
|
|
||||||
amount: p.amount != null ? Number(p.amount) : null
|
|
||||||
}));
|
}));
|
||||||
|
const removed = (delta.removedLines || []).map((ln) => ({
|
||||||
|
LineId: ln.LineId || null,
|
||||||
|
Sequence: ln.Sequence || ln.seq || null,
|
||||||
|
OpCode: upper(ln.OpCode || ln.opCode) || null,
|
||||||
|
Reason: sanitize(ln.Reason || ln.reason) || null
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totals = delta?.totals
|
||||||
|
? {
|
||||||
|
LaborTotal: asNumberOrNull(delta.totals.labor),
|
||||||
|
PartsTotal: asNumberOrNull(delta.totals.parts),
|
||||||
|
MiscTotal: asNumberOrNull(delta.totals.misc),
|
||||||
|
TaxTotal: asNumberOrNull(delta.totals.tax),
|
||||||
|
GrandTotal: asNumberOrNull(delta.totals.total)
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const insurance = delta?.insurance
|
||||||
|
? {
|
||||||
|
CompanyName: upper(delta.insurance.company),
|
||||||
|
ClaimNumber: sanitize(delta.insurance.claim),
|
||||||
|
AdjusterName: upper(delta.insurance.adjuster),
|
||||||
|
AdjusterPhone: sanitize(delta.insurance.phone)
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const notes =
|
||||||
|
Array.isArray(delta?.notes) && delta.notes.length
|
||||||
|
? { Items: delta.notes.map((n) => sanitize(n)).filter(Boolean) }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...dealer,
|
||||||
|
RequestId: delta?.RequestId || current?.RequestId || null,
|
||||||
|
Environment: process.env.NODE_ENV || "development",
|
||||||
|
|
||||||
|
RepairOrderId: current?.RepairOrderId || delta?.RepairOrderId || null,
|
||||||
|
RepairOrderNumber: delta?.RepairOrderNumber || current?.RepairOrderNumber || null,
|
||||||
|
Status: delta?.Status || null,
|
||||||
|
ROType: delta?.ROType || null,
|
||||||
|
OpenDate: delta?.OpenDate || null,
|
||||||
|
PromisedDate: delta?.PromisedDate || null,
|
||||||
|
CloseDate: delta?.CloseDate || null,
|
||||||
|
ServiceAdvisorId: delta?.ServiceAdvisorId || null,
|
||||||
|
TechnicianId: delta?.TechnicianId || null,
|
||||||
|
LocationCode: delta?.LocationCode || null,
|
||||||
|
Department: delta?.Department || null,
|
||||||
|
PurchaseOrder: delta?.PurchaseOrder || null,
|
||||||
|
|
||||||
|
// Optional customer/vehicle patches
|
||||||
|
Customer: delta?.Customer || null,
|
||||||
|
Vehicle: delta?.Vehicle || null,
|
||||||
|
|
||||||
|
// Line changes
|
||||||
|
AddedJobLines: added.length ? added : null,
|
||||||
|
UpdatedJobLines: updated.length ? updated : null,
|
||||||
|
RemovedJobLines: removed.length ? removed : null,
|
||||||
|
|
||||||
|
Totals: totals,
|
||||||
|
Insurance: insurance,
|
||||||
|
Notes: notes
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Exports -----------------------------------------------------------
|
/* ------------------------------- Line Mapping ------------------------------- */
|
||||||
|
|
||||||
|
function mapJobLineToRRLine(line, sequenceFallback, opts = {}) {
|
||||||
|
// opts.includePayType => include PayType when present (CUST|INS|WARR|INT)
|
||||||
|
const qty = asNumberOrNull(line?.Quantity || line?.qty || line?.part_qty || 1);
|
||||||
|
const unit = asNumberOrNull(line?.UnitPrice || line?.price || line?.unitPrice);
|
||||||
|
const ext = asNumberOrNull(line?.ExtendedPrice || (qty && unit ? qty * unit : line?.extended));
|
||||||
|
|
||||||
|
return {
|
||||||
|
Sequence: asNumberOrNull(line?.Sequence || line?.seq) || asNumberOrNull(sequenceFallback),
|
||||||
|
OpCode: upper(line?.OpCode || line?.opCode || line?.opcode),
|
||||||
|
Description: sanitize(line?.Description || line?.description || line?.desc || line?.story),
|
||||||
|
LaborHours: asNumberOrNull(line?.LaborHours || line?.laborHours),
|
||||||
|
LaborRate: asNumberOrNull(line?.LaborRate || line?.laborRate),
|
||||||
|
PartNumber: upper(line?.PartNumber || line?.partNumber || line?.part_no),
|
||||||
|
PartDescription: sanitize(line?.PartDescription || line?.partDescription || line?.part_desc),
|
||||||
|
Quantity: qty,
|
||||||
|
UnitPrice: unit,
|
||||||
|
ExtendedPrice: ext,
|
||||||
|
TaxCode: upper(line?.TaxCode || line?.taxCode) || null,
|
||||||
|
PayType: opts.includePayType ? upper(line?.PayType || line?.payType) || null : undefined,
|
||||||
|
Reason: sanitize(line?.Reason || line?.reason) || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Used by current call-sites:
|
// Customer
|
||||||
mapCustomerInsert,
|
mapCustomerInsert,
|
||||||
mapCustomerUpdate,
|
mapCustomerUpdate,
|
||||||
mapRepairOrderCreate,
|
|
||||||
mapRepairOrderUpdate,
|
|
||||||
|
|
||||||
// Extra scaffolds you’ll likely use soon:
|
// Vehicle
|
||||||
mapVehicleInsertFromJob,
|
mapVehicleInsertFromJob,
|
||||||
mapJobLineToRRLine,
|
|
||||||
mapTaxes,
|
|
||||||
mapPayments,
|
|
||||||
|
|
||||||
// Low-level utils (handy in tests)
|
// Repair orders
|
||||||
|
mapRepairOrderAddFromJob,
|
||||||
|
mapRepairOrderChangeFromJob,
|
||||||
|
mapJobLineToRRLine,
|
||||||
|
|
||||||
|
// shared utils (handy in tests)
|
||||||
|
buildDealerVars,
|
||||||
_sanitize: sanitize,
|
_sanitize: sanitize,
|
||||||
_normalizePostal: normalizePostal,
|
_upper: upper,
|
||||||
_toUpperOrNull: toUpperOrNull
|
_normalizePostal: normalizePostal
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,76 +1,144 @@
|
|||||||
// server/rr/rr-repair-orders.js
|
/**
|
||||||
// -----------------------------------------------------------------------------
|
* @file rr-repair-orders.js
|
||||||
// RR Repair Order create/update wired through MakeRRCall.
|
* @description Reynolds & Reynolds (Rome) Repair Order Create & Update.
|
||||||
// Mapping comes from rr-mappers.js and response validation via rr-error.js.
|
* Implements the "Create Body Shop Management Repair Order" and
|
||||||
//
|
* "Update Body Shop Management Repair Order" specifications.
|
||||||
// What’s still missing (complete when you wire to the PDFs):
|
*/
|
||||||
// - Final RR request envelopes & field names in rr-mappers.js
|
|
||||||
// (Create: “RepairOrderAddRq”, Update: “RepairOrderChgRq”, etc.)
|
|
||||||
// - Definitive success/error envelope checks in rr-error.js (assertRrOk)
|
|
||||||
// - Any RR-required headers (dealer/tenant/site/location ids) in rr-helpers
|
|
||||||
// - If RR requires path params for update (e.g., /repair-orders/{id}),
|
|
||||||
// either add requestPathParams here or move id into RRActions.UpdateRepairOrder
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
const { MakeRRCall, RRActions } = require("./rr-helpers");
|
||||||
const { assertRrOk } = require("./rr-error");
|
const { assertRrOk } = require("./rr-error");
|
||||||
const { mapRepairOrderAddFromJob, mapRepairOrderChangeFromJob } = require("./rr-mappers");
|
const { mapRepairOrderCreate, mapRepairOrderUpdate } = require("./rr-mappers");
|
||||||
|
const RRLogger = require("./rr-logger");
|
||||||
|
const { client } = require("../graphql-client/graphql-client");
|
||||||
|
const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Repair Order in RR.
|
* Fetch rr_configuration for the current bodyshop directly from DB.
|
||||||
|
* Dealer-specific configuration is mandatory for RR operations.
|
||||||
|
*/
|
||||||
|
async function getDealerConfigFromDB(bodyshopId, logger) {
|
||||||
|
try {
|
||||||
|
const result = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
|
||||||
|
const config = result?.bodyshops_by_pk?.rr_configuration || null;
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`No rr_configuration found for bodyshop ID ${bodyshopId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger?.debug?.(`Fetched rr_configuration for bodyshop ${bodyshopId}`, config);
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
logger?.log?.("rr-get-dealer-config", "ERROR", "rr", null, {
|
||||||
|
bodyshopId,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CREATE REPAIR ORDER
|
||||||
|
* Based on "Rome Create Body Shop Management Repair Order Specification"
|
||||||
*
|
*
|
||||||
* @param {Object} deps
|
* @param {object} options
|
||||||
* @param {Socket|ExpressRequest} deps.socket
|
* @param {object} options.socket - socket or express request
|
||||||
* @param {Object} deps.redisHelpers
|
* @param {object} options.redisHelpers
|
||||||
* @param {Object} deps.JobData - Rome job (used for mapping)
|
* @param {object} options.JobData - internal job object
|
||||||
* @param {Object} deps.txEnvelope - Posting/GL context if needed in mapping
|
* @param {object} [options.txEnvelope] - transaction metadata (advisor, timestamps, etc.)
|
||||||
* @returns {Promise<any>} - RR response (envelope TBD)
|
|
||||||
*/
|
*/
|
||||||
async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) {
|
async function CreateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) {
|
||||||
// Map JobData (+ optional txEnvelope) -> RR "Repair Order Add" request body
|
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||||
const body = mapRepairOrderAddFromJob({ ...JobData, txEnvelope });
|
const logger = socket?.logger || console;
|
||||||
|
|
||||||
const data = await MakeRRCall({
|
try {
|
||||||
...RRActions.CreateRepairOrder, // POST /repair-orders/v1
|
RRLogger(socket, "info", "RR Create Repair Order started", {
|
||||||
body,
|
jobid: JobData?.id,
|
||||||
redisHelpers,
|
bodyshopId
|
||||||
socket,
|
});
|
||||||
jobid: JobData?.id
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Update assertRrOk once RR’s success envelope is finalized
|
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
||||||
return assertRrOk(data, { apiName: "RR Create Repair Order" });
|
|
||||||
|
// Build Mustache variables for server/rr/xml-templates/CreateRepairOrder.xml
|
||||||
|
const vars = mapRepairOrderCreate({ JobData, txEnvelope, dealerConfig });
|
||||||
|
|
||||||
|
const data = await MakeRRCall({
|
||||||
|
action: RRActions.CreateRepairOrder, // resolves SOAPAction+URL
|
||||||
|
body: { template: "CreateRepairOrder", data: vars }, // render XML template
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid: JobData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = assertRrOk(data, { apiName: "RR Create Repair Order" });
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", "RR Create Repair Order success", {
|
||||||
|
jobid: JobData?.id,
|
||||||
|
dealer: dealerConfig?.dealer_code || dealerConfig?.dealerCode
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR Create Repair Order failed: ${error.message}`, {
|
||||||
|
jobid: JobData?.id
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a Repair Order in RR.
|
* UPDATE REPAIR ORDER
|
||||||
|
* Based on "Rome Update Body Shop Management Repair Order Specification"
|
||||||
*
|
*
|
||||||
* NOTE: If RR requires the repair order id in the URL (PUT /repair-orders/{id}),
|
* @param {object} options
|
||||||
* pass it via requestPathParams here once you have it:
|
* @param {object} options.socket
|
||||||
* requestPathParams: repairOrderId
|
* @param {object} options.redisHelpers
|
||||||
* and ensure RRActions.UpdateRepairOrder.url ends with a trailing slash.
|
* @param {object} options.JobData
|
||||||
*
|
* @param {object} [options.txEnvelope]
|
||||||
* @param {Object} deps
|
|
||||||
* @param {Socket|ExpressRequest} deps.socket
|
|
||||||
* @param {Object} deps.redisHelpers
|
|
||||||
* @param {Object} deps.JobData - Rome job (used for mapping)
|
|
||||||
* @param {Object} deps.txEnvelope - Posting/GL context if needed in mapping
|
|
||||||
* @param {string|number} [deps.repairOrderId] - If RR expects a path param
|
|
||||||
* @returns {Promise<any>} - RR response (envelope TBD)
|
|
||||||
*/
|
*/
|
||||||
async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope, repairOrderId }) {
|
async function UpdateRepairOrder({ socket, redisHelpers, JobData, txEnvelope }) {
|
||||||
const body = mapRepairOrderChangeFromJob({ ...JobData, txEnvelope });
|
const bodyshopId = socket?.bodyshopId || JobData?.bodyshopid;
|
||||||
|
const logger = socket?.logger || console;
|
||||||
|
|
||||||
const data = await MakeRRCall({
|
try {
|
||||||
...RRActions.UpdateRepairOrder, // PUT /repair-orders/v1 (or /v1/{id})
|
RRLogger(socket, "info", "RR Update Repair Order started", {
|
||||||
...(repairOrderId ? { requestPathParams: String(repairOrderId) } : {}),
|
jobid: JobData?.id,
|
||||||
body,
|
bodyshopId,
|
||||||
redisHelpers,
|
rr_ro_id: JobData?.rr_ro_id
|
||||||
socket,
|
});
|
||||||
jobid: JobData?.id
|
|
||||||
});
|
|
||||||
|
|
||||||
return assertRrOk(data, { apiName: "RR Update Repair Order" });
|
const dealerConfig = await getDealerConfigFromDB(bodyshopId, logger);
|
||||||
|
|
||||||
|
// Build Mustache variables for server/rr/xml-templates/UpdateRepairOrder.xml
|
||||||
|
const vars = mapRepairOrderUpdate({ JobData, txEnvelope, dealerConfig });
|
||||||
|
|
||||||
|
const data = await MakeRRCall({
|
||||||
|
action: RRActions.UpdateRepairOrder, // resolves SOAPAction+URL
|
||||||
|
body: { template: "UpdateRepairOrder", data: vars }, // render XML template
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid: JobData.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = assertRrOk(data, { apiName: "RR Update Repair Order" });
|
||||||
|
|
||||||
|
RRLogger(socket, "debug", "RR Update Repair Order success", {
|
||||||
|
jobid: JobData?.id,
|
||||||
|
rr_ro_id: JobData?.rr_ro_id
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
RRLogger(socket, "error", `RR Update Repair Order failed: ${error.message}`, {
|
||||||
|
jobid: JobData?.id,
|
||||||
|
rr_ro_id: JobData?.rr_ro_id
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { CreateRepairOrder, UpdateRepairOrder };
|
module.exports = {
|
||||||
|
CreateRepairOrder,
|
||||||
|
UpdateRepairOrder,
|
||||||
|
getDealerConfigFromDB
|
||||||
|
};
|
||||||
|
|||||||
127
server/rr/rr-test.js
Normal file
127
server/rr/rr-test.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// node server/rr/rr-test.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file rr-test.js
|
||||||
|
* @description Diagnostic test script for Reynolds & Reynolds (R&R) integration.
|
||||||
|
* Run with: NODE_ENV=development node server/rr/rr-test.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
require("dotenv").config({
|
||||||
|
path: path.resolve(__dirname, "../../", `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
const fs = require("fs/promises");
|
||||||
|
const mustache = require("mustache");
|
||||||
|
const { getBaseRRConfig } = require("./rr-constants");
|
||||||
|
const { RRActions, MakeRRCall } = require("./rr-helpers");
|
||||||
|
const RRLogger = require("./rr-logger");
|
||||||
|
|
||||||
|
// --- Mock socket + redis helpers for standalone test
|
||||||
|
const socket = {
|
||||||
|
bodyshopId: process.env.TEST_BODYSHOP_ID || null,
|
||||||
|
user: { email: "test@romeonline.io" },
|
||||||
|
emit: (event, data) => console.log(`[SOCKET EVENT] ${event}`, data),
|
||||||
|
logger: console
|
||||||
|
};
|
||||||
|
|
||||||
|
const redisHelpers = {
|
||||||
|
setSessionData: async () => {},
|
||||||
|
getSessionData: async () => {},
|
||||||
|
setSessionTransactionData: async () => {},
|
||||||
|
getSessionTransactionData: async () => {},
|
||||||
|
clearSessionTransactionData: async () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
console.log("=== Reynolds & Reynolds Integration Test ===");
|
||||||
|
console.log("NODE_ENV:", process.env.NODE_ENV);
|
||||||
|
|
||||||
|
const baseCfg = getBaseRRConfig();
|
||||||
|
console.log("Base R&R Config (from env):", {
|
||||||
|
baseUrl: baseCfg.baseUrl,
|
||||||
|
hasUser: !!baseCfg.username || !!process.env.RR_API_USER || !!process.env.RR_USERNAME,
|
||||||
|
hasPass: !!baseCfg.password || !!process.env.RR_API_PASS || !!process.env.RR_PASSWORD,
|
||||||
|
timeout: baseCfg.timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- test variables for GetAdvisors
|
||||||
|
const templateVars = {
|
||||||
|
DealerCode: process.env.RR_DEALER_NAME || "ROME",
|
||||||
|
DealerName: "Rome Collision Test",
|
||||||
|
SearchCriteria: {
|
||||||
|
Department: "Body Shop",
|
||||||
|
Status: "ACTIVE"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dealer/Store/Branch/PPSysId can come from rr_configuration or env; for test we override:
|
||||||
|
const dealerConfigOverride = {
|
||||||
|
// baseUrl can also be overridden here if you want
|
||||||
|
ppsysid: process.env.RR_PPSYSID || process.env.RR_PP_SYS_ID || process.env.RR_PP_SYSID || "TEST-PPSYSID",
|
||||||
|
dealer_number: process.env.RR_DEALER_NUMBER || "12345",
|
||||||
|
store_number: process.env.RR_STORE_NUMBER || "01",
|
||||||
|
branch_number: process.env.RR_BRANCH_NUMBER || "001",
|
||||||
|
// creds (optional here; MakeRRCall will fallback to env if omitted)
|
||||||
|
username: process.env.RR_API_USER || process.env.RR_USERNAME || "Rome",
|
||||||
|
password: process.env.RR_API_PASS || process.env.RR_PASSWORD || "secret"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the first ~600 chars of the envelope we will send (by rendering the template + header)
|
||||||
|
// NOTE: This is just for printing; MakeRRCall will rebuild with proper header internally.
|
||||||
|
const templatePath = path.join(__dirname, "xml-templates", "GetAdvisors.xml");
|
||||||
|
const tpl = await fs.readFile(templatePath, "utf8");
|
||||||
|
const renderedBody = mustache.render(tpl, templateVars);
|
||||||
|
|
||||||
|
// Build a preview envelope using the same helper used by MakeRRCall
|
||||||
|
const { renderXmlTemplate } = require("./rr-helpers");
|
||||||
|
const headerPreview = await renderXmlTemplate("_EnvelopeHeader", {
|
||||||
|
PPSysId: dealerConfigOverride.ppsysid,
|
||||||
|
DealerNumber: dealerConfigOverride.dealer_number,
|
||||||
|
StoreNumber: dealerConfigOverride.store_number,
|
||||||
|
BranchNumber: dealerConfigOverride.branch_number,
|
||||||
|
Username: dealerConfigOverride.username,
|
||||||
|
Password: dealerConfigOverride.password,
|
||||||
|
CorrelationId: "preview-correlation"
|
||||||
|
});
|
||||||
|
const previewEnvelope = `
|
||||||
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<soapenv:Header>
|
||||||
|
${headerPreview}
|
||||||
|
</soapenv:Header>
|
||||||
|
<soapenv:Body>
|
||||||
|
${renderedBody}
|
||||||
|
</soapenv:Body>
|
||||||
|
</soapenv:Envelope>`.trim();
|
||||||
|
|
||||||
|
console.log("\n--- Rendered SOAP Envelope (first 600 chars) ---\n");
|
||||||
|
console.log(previewEnvelope.slice(0, 600));
|
||||||
|
console.log("... [truncated]\n");
|
||||||
|
|
||||||
|
// If we don't have a base URL, skip the live call
|
||||||
|
if (!baseCfg.baseUrl) {
|
||||||
|
console.warn("\n⚠️ No RR baseUrl defined. Skipping live call.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`--- Sending SOAP Request: ${RRActions.GetAdvisors.action} ---\n`);
|
||||||
|
|
||||||
|
const responseXml = await MakeRRCall({
|
||||||
|
action: "GetAdvisors",
|
||||||
|
baseUrl: process.env.RR_API_BASE_URL,
|
||||||
|
body: { template: "GetAdvisors", data: templateVars },
|
||||||
|
dealerConfig: dealerConfigOverride,
|
||||||
|
redisHelpers,
|
||||||
|
socket,
|
||||||
|
jobid: "test-job",
|
||||||
|
retries: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
RRLogger(socket, "info", "RR test successful", { bytes: Buffer.byteLength(responseXml, "utf8") });
|
||||||
|
console.log("\n✅ Test completed successfully.\n");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("\n❌ Test failed:", error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
}
|
||||||
|
})();
|
||||||
97
server/rr/rr-wsdl.js
Normal file
97
server/rr/rr-wsdl.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* RR WSDL / SOAP XML Transport Layer (thin wrapper)
|
||||||
|
* -------------------------------------------------
|
||||||
|
* Delegates to rr-helpers.MakeRRCall (which handles:
|
||||||
|
* - fetching dealer config from DB via resolveRRConfig
|
||||||
|
* - rendering Mustache XML templates
|
||||||
|
* - building SOAP envelope + headers
|
||||||
|
* - axios POST + retries
|
||||||
|
*
|
||||||
|
* Use this when you prefer the "action + variables" style and (optionally)
|
||||||
|
* want a parsed Body node back instead of raw XML.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { XMLParser } = require("fast-xml-parser");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const { MakeRRCall, resolveRRConfig, renderXmlTemplate } = require("./rr-helpers");
|
||||||
|
|
||||||
|
// Map friendly action names to template filenames (no envelope here; helpers add it)
|
||||||
|
const RR_ACTION_MAP = {
|
||||||
|
CustomerInsert: { file: "InsertCustomer.xml" },
|
||||||
|
CustomerUpdate: { file: "UpdateCustomer.xml" },
|
||||||
|
ServiceVehicleInsert: { file: "InsertServiceVehicle.xml" },
|
||||||
|
CombinedSearch: { file: "CombinedSearch.xml" },
|
||||||
|
GetParts: { file: "GetParts.xml" },
|
||||||
|
GetAdvisors: { file: "GetAdvisors.xml" },
|
||||||
|
CreateRepairOrder: { file: "CreateRepairOrder.xml" },
|
||||||
|
UpdateRepairOrder: { file: "UpdateRepairOrder.xml" }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionally render just the body XML for a given action (no SOAP envelope).
|
||||||
|
* Mostly useful for diagnostics/tests.
|
||||||
|
*/
|
||||||
|
async function buildRRXml(action, variables = {}) {
|
||||||
|
const entry = RR_ACTION_MAP[action];
|
||||||
|
if (!entry) throw new Error(`Unknown RR action: ${action}`);
|
||||||
|
const templateName = entry.file.replace(/\.xml$/i, "");
|
||||||
|
return renderXmlTemplate(templateName, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an RR SOAP request using helpers (action + variables).
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {string} opts.action One of RR_ACTION_MAP keys (and RR_ACTIONS in rr-constants)
|
||||||
|
* @param {object} opts.variables Mustache variables for the body template
|
||||||
|
* @param {object} opts.socket Socket/req for context (bodyshopId + auth)
|
||||||
|
* @param {boolean} [opts.raw=false] If true, returns raw XML string
|
||||||
|
* @param {number} [opts.retries=1] Transient retry attempts (5xx/network)
|
||||||
|
* @returns {Promise<string|object>} Raw XML (raw=true) or parsed Body node
|
||||||
|
*/
|
||||||
|
async function sendRRRequest({ action, variables = {}, socket, raw = false, retries = 1 }) {
|
||||||
|
const entry = RR_ACTION_MAP[action];
|
||||||
|
if (!entry) throw new Error(`Unknown RR action: ${action}`);
|
||||||
|
|
||||||
|
const templateName = entry.file.replace(/\.xml$/i, "");
|
||||||
|
const dealerConfig = await resolveRRConfig(socket);
|
||||||
|
|
||||||
|
// Let MakeRRCall render + envelope + post
|
||||||
|
const xml = await MakeRRCall({
|
||||||
|
action,
|
||||||
|
body: { template: templateName, data: variables },
|
||||||
|
socket,
|
||||||
|
dealerConfig,
|
||||||
|
retries
|
||||||
|
});
|
||||||
|
|
||||||
|
if (raw) return xml;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parser = new XMLParser({ ignoreAttributes: false });
|
||||||
|
const parsed = parser.parse(xml);
|
||||||
|
|
||||||
|
// Try several common namespace variants for Envelope/Body
|
||||||
|
const bodyNode =
|
||||||
|
parsed?.Envelope?.Body ||
|
||||||
|
parsed?.["soapenv:Envelope"]?.["soapenv:Body"] ||
|
||||||
|
parsed?.["SOAP-ENV:Envelope"]?.["SOAP-ENV:Body"] ||
|
||||||
|
parsed?.["S:Envelope"]?.["S:Body"] ||
|
||||||
|
parsed;
|
||||||
|
|
||||||
|
return bodyNode;
|
||||||
|
} catch (err) {
|
||||||
|
logger.log("rr-wsdl-parse-error", "ERROR", "RR", null, {
|
||||||
|
action,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack
|
||||||
|
});
|
||||||
|
// If parsing fails, return raw so caller can inspect
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendRRRequest,
|
||||||
|
buildRRXml,
|
||||||
|
RR_ACTION_MAP
|
||||||
|
};
|
||||||
73
server/rr/xml-templates/CombinedSearch.xml
Normal file
73
server/rr/xml-templates/CombinedSearch.xml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<rr:CombinedSearchRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}
|
||||||
|
<rr:RequestId>{{RequestId}}</rr:RequestId>
|
||||||
|
{{/RequestId}}
|
||||||
|
{{#Environment}}
|
||||||
|
<rr:Environment>{{Environment}}</rr:Environment>
|
||||||
|
{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}
|
||||||
|
<rr:DealerName>{{DealerName}}</rr:DealerName>
|
||||||
|
{{/DealerName}}
|
||||||
|
{{#DealerNumber}}
|
||||||
|
<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>
|
||||||
|
{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}
|
||||||
|
<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>
|
||||||
|
{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}
|
||||||
|
<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>
|
||||||
|
{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
<rr:SearchCriteria>
|
||||||
|
{{#Customer}}
|
||||||
|
<rr:Customer>
|
||||||
|
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||||
|
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||||
|
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||||
|
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||||
|
</rr:Customer>
|
||||||
|
{{/Customer}}
|
||||||
|
|
||||||
|
{{#Vehicle}}
|
||||||
|
<rr:ServiceVehicle>
|
||||||
|
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||||
|
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||||
|
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||||
|
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||||
|
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||||
|
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
|
||||||
|
</rr:ServiceVehicle>
|
||||||
|
{{/Vehicle}}
|
||||||
|
|
||||||
|
{{#Company}}
|
||||||
|
<rr:Company>
|
||||||
|
{{#Name}}<rr:Name>{{Name}}</rr:Name>{{/Name}}
|
||||||
|
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
|
||||||
|
</rr:Company>
|
||||||
|
{{/Company}}
|
||||||
|
|
||||||
|
<!-- Search behavior flags (all optional) -->
|
||||||
|
{{#SearchMode}}<rr:SearchMode>{{SearchMode}}</rr:SearchMode>{{/SearchMode}}
|
||||||
|
{{#ExactMatch}}<rr:ExactMatch>{{ExactMatch}}</rr:ExactMatch>{{/ExactMatch}}
|
||||||
|
{{#PartialMatch}}<rr:PartialMatch>{{PartialMatch}}</rr:PartialMatch>{{/PartialMatch}}
|
||||||
|
{{#CaseInsensitive}}<rr:CaseInsensitive>{{CaseInsensitive}}</rr:CaseInsensitive>{{/CaseInsensitive}}
|
||||||
|
|
||||||
|
<!-- Result shaping (all optional) -->
|
||||||
|
{{#ReturnCustomers}}<rr:ReturnCustomers>{{ReturnCustomers}}</rr:ReturnCustomers>{{/ReturnCustomers}}
|
||||||
|
{{#ReturnVehicles}}<rr:ReturnVehicles>{{ReturnVehicles}}</rr:ReturnVehicles>{{/ReturnVehicles}}
|
||||||
|
{{#ReturnCompanies}}<rr:ReturnCompanies>{{ReturnCompanies}}</rr:ReturnCompanies>{{/ReturnCompanies}}
|
||||||
|
|
||||||
|
<!-- Paging/sorting (all optional) -->
|
||||||
|
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
||||||
|
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
||||||
|
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
|
||||||
|
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
|
||||||
|
</rr:SearchCriteria>
|
||||||
|
</rr:CombinedSearchRq>
|
||||||
158
server/rr/xml-templates/CreateRepairOrder.xml
Normal file
158
server/rr/xml-templates/CreateRepairOrder.xml
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<rr:RepairOrderInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||||
|
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||||
|
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
<rr:RepairOrder>
|
||||||
|
<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>
|
||||||
|
{{#DmsRepairOrderId}}<rr:DmsRepairOrderId>{{DmsRepairOrderId}}</rr:DmsRepairOrderId>{{/DmsRepairOrderId}}
|
||||||
|
|
||||||
|
<!-- Core dates -->
|
||||||
|
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
||||||
|
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
||||||
|
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
||||||
|
|
||||||
|
<!-- People & routing -->
|
||||||
|
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||||
|
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
||||||
|
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||||
|
{{#ProfitCenter}}<rr:ProfitCenter>{{ProfitCenter}}</rr:ProfitCenter>{{/ProfitCenter}}
|
||||||
|
|
||||||
|
<!-- Type & status -->
|
||||||
|
{{#ROType}}<rr:ROType>{{ROType}}</rr:ROType>{{/ROType}}
|
||||||
|
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}}
|
||||||
|
{{#IsBodyShop}}<rr:IsBodyShop>{{IsBodyShop}}</rr:IsBodyShop>{{/IsBodyShop}}
|
||||||
|
{{#DRPFlag}}<rr:DRPFlag>{{DRPFlag}}</rr:DRPFlag>{{/DRPFlag}}
|
||||||
|
|
||||||
|
<!-- Customer -->
|
||||||
|
<rr:Customer>
|
||||||
|
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
|
||||||
|
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
||||||
|
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||||
|
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||||
|
|
||||||
|
<!-- Optional address if you have it -->
|
||||||
|
{{#Address}}
|
||||||
|
<rr:Address>
|
||||||
|
{{#Line1}}<rr:Line1>{{Line1}}</rr:Line1>{{/Line1}}
|
||||||
|
{{#Line2}}<rr:Line2>{{Line2}}</rr:Line2>{{/Line2}}
|
||||||
|
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
||||||
|
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
||||||
|
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
||||||
|
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
||||||
|
</rr:Address>
|
||||||
|
{{/Address}}
|
||||||
|
</rr:Customer>
|
||||||
|
|
||||||
|
<!-- Vehicle -->
|
||||||
|
<rr:Vehicle>
|
||||||
|
{{#VehicleId}}<rr:VehicleId>{{VehicleId}}</rr:VehicleId>{{/VehicleId}}
|
||||||
|
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||||
|
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||||
|
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||||
|
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||||
|
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||||
|
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||||
|
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||||
|
</rr:Vehicle>
|
||||||
|
|
||||||
|
<!-- Job lines -->
|
||||||
|
{{#JobLines}}
|
||||||
|
<rr:JobLine>
|
||||||
|
<rr:Sequence>{{Sequence}}</rr:Sequence>
|
||||||
|
{{#ParentSequence}}<rr:ParentSequence>{{ParentSequence}}</rr:ParentSequence>{{/ParentSequence}}
|
||||||
|
|
||||||
|
{{#LineType}}<rr:LineType>
|
||||||
|
{{LineType}}</rr:LineType>{{/LineType}} <!-- LABOR | PART | MISC | FEE | DISCOUNT -->
|
||||||
|
{{#Category}}<rr:Category>
|
||||||
|
{{Category}}</rr:Category>{{/Category}} <!-- e.g., BODY, PAINT, GLASS -->
|
||||||
|
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||||
|
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||||
|
|
||||||
|
<!-- Labor fields -->
|
||||||
|
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||||
|
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||||
|
|
||||||
|
<!-- Part fields -->
|
||||||
|
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||||
|
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||||
|
|
||||||
|
<!-- Amounts -->
|
||||||
|
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||||
|
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||||
|
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||||
|
{{#DiscountAmount}}<rr:DiscountAmount>{{DiscountAmount}}</rr:DiscountAmount>{{/DiscountAmount}}
|
||||||
|
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||||
|
{{#GLAccount}}<rr:GLAccount>{{GLAccount}}</rr:GLAccount>{{/GLAccount}}
|
||||||
|
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
|
||||||
|
|
||||||
|
<!-- Tax details (optional) -->
|
||||||
|
{{#Taxes}}
|
||||||
|
<rr:Taxes>
|
||||||
|
{{#Items}}
|
||||||
|
<rr:Tax>
|
||||||
|
<rr:Code>{{Code}}</rr:Code>
|
||||||
|
<rr:Amount>{{Amount}}</rr:Amount>
|
||||||
|
{{#Rate}}<rr:Rate>{{Rate}}</rr:Rate>{{/Rate}}
|
||||||
|
</rr:Tax>
|
||||||
|
{{/Items}}
|
||||||
|
</rr:Taxes>
|
||||||
|
{{/Taxes}}
|
||||||
|
</rr:JobLine>
|
||||||
|
{{/JobLines}}
|
||||||
|
|
||||||
|
<!-- Totals -->
|
||||||
|
{{#Totals}}
|
||||||
|
<rr:Totals>
|
||||||
|
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
|
||||||
|
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
||||||
|
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
||||||
|
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
||||||
|
{{#DiscountTotal}}<rr:DiscountTotal>{{DiscountTotal}}</rr:DiscountTotal>{{/DiscountTotal}}
|
||||||
|
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
||||||
|
<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>
|
||||||
|
</rr:Totals>
|
||||||
|
{{/Totals}}
|
||||||
|
|
||||||
|
<!-- Payers/Payments (optional) -->
|
||||||
|
{{#Payments}}
|
||||||
|
<rr:Payments>
|
||||||
|
{{#Items}}
|
||||||
|
<rr:Payment>
|
||||||
|
<rr:PayerType>{{PayerType}}</rr:PayerType> <!-- CUSTOMER | INSURANCE | WARRANTY | FLEET -->
|
||||||
|
{{#PayerName}}<rr:PayerName>{{PayerName}}</rr:PayerName>{{/PayerName}}
|
||||||
|
<rr:Amount>{{Amount}}</rr:Amount>
|
||||||
|
{{#Method}}<rr:Method>{{Method}}</rr:Method>{{/Method}}
|
||||||
|
{{#Reference}}<rr:Reference>{{Reference}}</rr:Reference>{{/Reference}}
|
||||||
|
{{#ControlNumber}}<rr:ControlNumber>{{ControlNumber}}</rr:ControlNumber>{{/ControlNumber}}
|
||||||
|
</rr:Payment>
|
||||||
|
{{/Items}}
|
||||||
|
</rr:Payments>
|
||||||
|
{{/Payments}}
|
||||||
|
|
||||||
|
<!-- Insurance block (optional) -->
|
||||||
|
{{#Insurance}}
|
||||||
|
<rr:Insurance>
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}}
|
||||||
|
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}}
|
||||||
|
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}}
|
||||||
|
</rr:Insurance>
|
||||||
|
{{/Insurance}}
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
{{#Notes}}
|
||||||
|
<rr:Notes>
|
||||||
|
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||||
|
</rr:Notes>
|
||||||
|
{{/Notes}}
|
||||||
|
</rr:RepairOrder>
|
||||||
|
</rr:RepairOrderInsertRq>
|
||||||
34
server/rr/xml-templates/GetAdvisors.xml
Normal file
34
server/rr/xml-templates/GetAdvisors.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<rr:GetAdvisorsRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||||
|
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||||
|
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
{{#SearchCriteria}}
|
||||||
|
<rr:SearchCriteria>
|
||||||
|
{{#AdvisorId}}<rr:AdvisorId>{{AdvisorId}}</rr:AdvisorId>{{/AdvisorId}}
|
||||||
|
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||||
|
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||||
|
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||||
|
{{#Status}}<rr:Status>{{Status}}</rr:Status>{{/Status}} <!-- ACTIVE | INACTIVE -->
|
||||||
|
{{#SearchMode}}<rr:SearchMode>
|
||||||
|
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
|
||||||
|
{{#Email}}<rr:Email>{{Email}}</rr:Email>{{/Email}}
|
||||||
|
{{#Phone}}<rr:Phone>{{Phone}}</rr:Phone>{{/Phone}}
|
||||||
|
{{#IncludeInactive}}<rr:IncludeInactive>{{IncludeInactive}}</rr:IncludeInactive>{{/IncludeInactive}}
|
||||||
|
|
||||||
|
<!-- Optional paging/sorting -->
|
||||||
|
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
||||||
|
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
||||||
|
{{#SortBy}}<rr:SortBy>{{SortBy}}</rr:SortBy>{{/SortBy}}
|
||||||
|
{{#SortDirection}}<rr:SortDirection>{{SortDirection}}</rr:SortDirection>{{/SortDirection}}
|
||||||
|
</rr:SearchCriteria>
|
||||||
|
{{/SearchCriteria}}
|
||||||
|
</rr:GetAdvisorsRq>
|
||||||
50
server/rr/xml-templates/GetParts.xml
Normal file
50
server/rr/xml-templates/GetParts.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<rr:GetPartRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||||
|
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||||
|
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
<rr:SearchCriteria>
|
||||||
|
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||||
|
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||||
|
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||||
|
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||||
|
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||||
|
{{#Vendor}}<rr:Vendor>{{Vendor}}</rr:Vendor>{{/Vendor}}
|
||||||
|
{{#Category}}<rr:Category>{{Category}}</rr:Category>{{/Category}}
|
||||||
|
|
||||||
|
<!-- Optional classification flags -->
|
||||||
|
{{#Brand}}<rr:Brand>{{Brand}}</rr:Brand>{{/Brand}}
|
||||||
|
{{#IsOEM}}<rr:IsOEM>{{IsOEM}}</rr:IsOEM>{{/IsOEM}} <!-- true | false -->
|
||||||
|
{{#IsAftermarket}}<rr:IsAftermarket>{{IsAftermarket}}</rr:IsAftermarket>{{/IsAftermarket}}
|
||||||
|
|
||||||
|
<!-- Availability / inventory -->
|
||||||
|
{{#InStock}}<rr:InStock>{{InStock}}</rr:InStock>{{/InStock}} <!-- true | false -->
|
||||||
|
{{#Warehouse}}<rr:Warehouse>{{Warehouse}}</rr:Warehouse>{{/Warehouse}}
|
||||||
|
{{#Location}}<rr:Location>{{Location}}</rr:Location>{{/Location}}
|
||||||
|
|
||||||
|
<!-- Pricing filters -->
|
||||||
|
{{#MinPrice}}<rr:MinPrice>{{MinPrice}}</rr:MinPrice>{{/MinPrice}}
|
||||||
|
{{#MaxPrice}}<rr:MaxPrice>{{MaxPrice}}</rr:MaxPrice>{{/MaxPrice}}
|
||||||
|
{{#Currency}}<rr:Currency>{{Currency}}</rr:Currency>{{/Currency}}
|
||||||
|
|
||||||
|
<!-- Search behavior -->
|
||||||
|
{{#SearchMode}}<rr:SearchMode>
|
||||||
|
{{SearchMode}}</rr:SearchMode>{{/SearchMode}} <!-- EXACT | PARTIAL -->
|
||||||
|
|
||||||
|
<!-- Paging / sorting -->
|
||||||
|
{{#MaxResults}}<rr:MaxResults>{{MaxResults}}</rr:MaxResults>{{/MaxResults}}
|
||||||
|
{{#PageNumber}}<rr:PageNumber>{{PageNumber}}</rr:PageNumber>{{/PageNumber}}
|
||||||
|
{{#SortBy}}<rr:SortBy>
|
||||||
|
{{SortBy}}</rr:SortBy>{{/SortBy}} <!-- e.g., PARTNUMBER, DESCRIPTION, PRICE -->
|
||||||
|
{{#SortDirection}}<rr:SortDirection>
|
||||||
|
{{SortDirection}}</rr:SortDirection>{{/SortDirection}} <!-- ASC | DESC -->
|
||||||
|
</rr:SearchCriteria>
|
||||||
|
</rr:GetPartRq>
|
||||||
102
server/rr/xml-templates/InsertCustomer.xml
Normal file
102
server/rr/xml-templates/InsertCustomer.xml
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<rr:CustomerInsertRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||||
|
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||||
|
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
<rr:Customer>
|
||||||
|
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||||
|
{{#CustomerType}}<rr:CustomerType>
|
||||||
|
{{CustomerType}}</rr:CustomerType>{{/CustomerType}} <!-- RETAIL | FLEET | INTERNAL -->
|
||||||
|
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||||
|
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}}
|
||||||
|
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||||
|
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}}
|
||||||
|
|
||||||
|
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}}
|
||||||
|
|
||||||
|
<!-- Optional customer classification -->
|
||||||
|
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}}
|
||||||
|
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}}
|
||||||
|
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}}
|
||||||
|
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}}
|
||||||
|
|
||||||
|
<!-- Addresses -->
|
||||||
|
{{#Addresses}}
|
||||||
|
<rr:Address>
|
||||||
|
{{#AddressType}}<rr:AddressType>
|
||||||
|
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING -->
|
||||||
|
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}}
|
||||||
|
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}}
|
||||||
|
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
||||||
|
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
||||||
|
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
||||||
|
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
||||||
|
</rr:Address>
|
||||||
|
{{/Addresses}}
|
||||||
|
|
||||||
|
<!-- Phones -->
|
||||||
|
{{#Phones}}
|
||||||
|
<rr:Phone>
|
||||||
|
<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>
|
||||||
|
{{#PhoneType}}<rr:PhoneType>
|
||||||
|
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
|
||||||
|
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||||
|
</rr:Phone>
|
||||||
|
{{/Phones}}
|
||||||
|
|
||||||
|
<!-- Emails -->
|
||||||
|
{{#Emails}}
|
||||||
|
<rr:Email>
|
||||||
|
<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>
|
||||||
|
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
|
||||||
|
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||||
|
</rr:Email>
|
||||||
|
{{/Emails}}
|
||||||
|
|
||||||
|
<!-- Driver's License -->
|
||||||
|
{{#DriverLicense}}
|
||||||
|
<rr:DriverLicense>
|
||||||
|
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
|
||||||
|
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
||||||
|
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||||
|
</rr:DriverLicense>
|
||||||
|
{{/DriverLicense}}
|
||||||
|
|
||||||
|
<!-- Insurance -->
|
||||||
|
{{#Insurance}}
|
||||||
|
<rr:Insurance>
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
||||||
|
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||||
|
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
||||||
|
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
||||||
|
</rr:Insurance>
|
||||||
|
{{/Insurance}}
|
||||||
|
|
||||||
|
<!-- Optional linked accounts -->
|
||||||
|
{{#LinkedAccounts}}
|
||||||
|
<rr:LinkedAccount>
|
||||||
|
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
|
||||||
|
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
|
||||||
|
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
|
||||||
|
</rr:LinkedAccount>
|
||||||
|
{{/LinkedAccounts}}
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
{{#Notes}}
|
||||||
|
<rr:Notes>
|
||||||
|
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||||
|
</rr:Notes>
|
||||||
|
{{/Notes}}
|
||||||
|
</rr:Customer>
|
||||||
|
</rr:CustomerInsertRq>
|
||||||
83
server/rr/xml-templates/InsertServiceVehicle.xml
Normal file
83
server/rr/xml-templates/InsertServiceVehicle.xml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<rr:ServiceVehicleAddRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||||
|
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||||
|
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
<rr:ServiceVehicle>
|
||||||
|
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||||
|
|
||||||
|
<!-- Identity -->
|
||||||
|
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||||
|
{{#UnitNumber}}<rr:UnitNumber>{{UnitNumber}}</rr:UnitNumber>{{/UnitNumber}}
|
||||||
|
{{#StockNumber}}<rr:StockNumber>{{StockNumber}}</rr:StockNumber>{{/StockNumber}}
|
||||||
|
|
||||||
|
<!-- Descriptive -->
|
||||||
|
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||||
|
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||||
|
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||||
|
{{#Trim}}<rr:Trim>{{Trim}}</rr:Trim>{{/Trim}}
|
||||||
|
{{#BodyStyle}}<rr:BodyStyle>{{BodyStyle}}</rr:BodyStyle>{{/BodyStyle}}
|
||||||
|
{{#Transmission}}<rr:Transmission>{{Transmission}}</rr:Transmission>{{/Transmission}}
|
||||||
|
{{#Engine}}<rr:Engine>{{Engine}}</rr:Engine>{{/Engine}}
|
||||||
|
{{#FuelType}}<rr:FuelType>{{FuelType}}</rr:FuelType>{{/FuelType}}
|
||||||
|
{{#DriveType}}<rr:DriveType>{{DriveType}}</rr:DriveType>{{/DriveType}}
|
||||||
|
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||||
|
|
||||||
|
<!-- Registration -->
|
||||||
|
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||||
|
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
||||||
|
{{#RegistrationExpiry}}<rr:RegistrationExpiry>{{RegistrationExpiry}}</rr:RegistrationExpiry>{{/RegistrationExpiry}}
|
||||||
|
|
||||||
|
<!-- Odometer -->
|
||||||
|
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||||
|
{{#OdometerUnits}}<rr:OdometerUnits>
|
||||||
|
{{OdometerUnits}}</rr:OdometerUnits>{{/OdometerUnits}} <!-- MI | KM -->
|
||||||
|
{{#InServiceDate}}<rr:InServiceDate>{{InServiceDate}}</rr:InServiceDate>{{/InServiceDate}}
|
||||||
|
|
||||||
|
<!-- Ownership -->
|
||||||
|
{{#Ownership}}
|
||||||
|
<rr:Ownership>
|
||||||
|
{{#OwnerId}}<rr:OwnerId>{{OwnerId}}</rr:OwnerId>{{/OwnerId}}
|
||||||
|
{{#OwnerName}}<rr:OwnerName>{{OwnerName}}</rr:OwnerName>{{/OwnerName}}
|
||||||
|
{{#OwnershipType}}<rr:OwnershipType>
|
||||||
|
{{OwnershipType}}</rr:OwnershipType>{{/OwnershipType}} <!-- OWNER | LEASED | FLEET -->
|
||||||
|
</rr:Ownership>
|
||||||
|
{{/Ownership}}
|
||||||
|
|
||||||
|
<!-- Insurance -->
|
||||||
|
{{#Insurance}}
|
||||||
|
<rr:Insurance>
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
||||||
|
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||||
|
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
||||||
|
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
||||||
|
</rr:Insurance>
|
||||||
|
{{/Insurance}}
|
||||||
|
|
||||||
|
<!-- Warranty -->
|
||||||
|
{{#Warranty}}
|
||||||
|
<rr:Warranty>
|
||||||
|
{{#WarrantyCompany}}<rr:WarrantyCompany>{{WarrantyCompany}}</rr:WarrantyCompany>{{/WarrantyCompany}}
|
||||||
|
{{#WarrantyNumber}}<rr:WarrantyNumber>{{WarrantyNumber}}</rr:WarrantyNumber>{{/WarrantyNumber}}
|
||||||
|
{{#WarrantyType}}<rr:WarrantyType>{{WarrantyType}}</rr:WarrantyType>{{/WarrantyType}}
|
||||||
|
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||||
|
</rr:Warranty>
|
||||||
|
{{/Warranty}}
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
{{#VehicleNotes}}
|
||||||
|
<rr:Notes>
|
||||||
|
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||||
|
</rr:Notes>
|
||||||
|
{{/VehicleNotes}}
|
||||||
|
</rr:ServiceVehicle>
|
||||||
|
</rr:ServiceVehicleAddRq>
|
||||||
107
server/rr/xml-templates/UpdateCustomer.xml
Normal file
107
server/rr/xml-templates/UpdateCustomer.xml
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<rr:CustomerUpdateRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||||
|
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||||
|
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
<rr:Customer>
|
||||||
|
<rr:CustomerId>{{CustomerId}}</rr:CustomerId>
|
||||||
|
{{#CustomerType}}<rr:CustomerType>{{CustomerType}}</rr:CustomerType>{{/CustomerType}}
|
||||||
|
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#FirstName}}<rr:FirstName>{{FirstName}}</rr:FirstName>{{/FirstName}}
|
||||||
|
{{#MiddleName}}<rr:MiddleName>{{MiddleName}}</rr:MiddleName>{{/MiddleName}}
|
||||||
|
{{#LastName}}<rr:LastName>{{LastName}}</rr:LastName>{{/LastName}}
|
||||||
|
{{#PreferredName}}<rr:PreferredName>{{PreferredName}}</rr:PreferredName>{{/PreferredName}}
|
||||||
|
|
||||||
|
{{#ActiveFlag}}<rr:ActiveFlag>{{ActiveFlag}}</rr:ActiveFlag>{{/ActiveFlag}}
|
||||||
|
{{#CustomerGroup}}<rr:CustomerGroup>{{CustomerGroup}}</rr:CustomerGroup>{{/CustomerGroup}}
|
||||||
|
{{#TaxExempt}}<rr:TaxExempt>{{TaxExempt}}</rr:TaxExempt>{{/TaxExempt}}
|
||||||
|
{{#DiscountLevel}}<rr:DiscountLevel>{{DiscountLevel}}</rr:DiscountLevel>{{/DiscountLevel}}
|
||||||
|
{{#PreferredLanguage}}<rr:PreferredLanguage>{{PreferredLanguage}}</rr:PreferredLanguage>{{/PreferredLanguage}}
|
||||||
|
|
||||||
|
<!-- Addresses -->
|
||||||
|
{{#Addresses}}
|
||||||
|
<rr:Address>
|
||||||
|
{{#AddressId}}<rr:AddressId>{{AddressId}}</rr:AddressId>{{/AddressId}}
|
||||||
|
{{#AddressType}}<rr:AddressType>
|
||||||
|
{{AddressType}}</rr:AddressType>{{/AddressType}} <!-- BILLING | MAILING | SHIPPING -->
|
||||||
|
{{#AddressLine1}}<rr:AddressLine1>{{AddressLine1}}</rr:AddressLine1>{{/AddressLine1}}
|
||||||
|
{{#AddressLine2}}<rr:AddressLine2>{{AddressLine2}}</rr:AddressLine2>{{/AddressLine2}}
|
||||||
|
{{#City}}<rr:City>{{City}}</rr:City>{{/City}}
|
||||||
|
{{#State}}<rr:State>{{State}}</rr:State>{{/State}}
|
||||||
|
{{#PostalCode}}<rr:PostalCode>{{PostalCode}}</rr:PostalCode>{{/PostalCode}}
|
||||||
|
{{#Country}}<rr:Country>{{Country}}</rr:Country>{{/Country}}
|
||||||
|
{{#IsPrimary}}<rr:IsPrimary>{{IsPrimary}}</rr:IsPrimary>{{/IsPrimary}}
|
||||||
|
</rr:Address>
|
||||||
|
{{/Addresses}}
|
||||||
|
|
||||||
|
<!-- Phones -->
|
||||||
|
{{#Phones}}
|
||||||
|
<rr:Phone>
|
||||||
|
{{#PhoneId}}<rr:PhoneId>{{PhoneId}}</rr:PhoneId>{{/PhoneId}}
|
||||||
|
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||||
|
{{#PhoneType}}<rr:PhoneType>
|
||||||
|
{{PhoneType}}</rr:PhoneType>{{/PhoneType}} <!-- MOBILE | HOME | WORK -->
|
||||||
|
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||||
|
{{#IsDeleted}}<rr:IsDeleted>
|
||||||
|
{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}} <!-- Mark for deletion -->
|
||||||
|
</rr:Phone>
|
||||||
|
{{/Phones}}
|
||||||
|
|
||||||
|
<!-- Emails -->
|
||||||
|
{{#Emails}}
|
||||||
|
<rr:Email>
|
||||||
|
{{#EmailId}}<rr:EmailId>{{EmailId}}</rr:EmailId>{{/EmailId}}
|
||||||
|
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||||
|
{{#EmailType}}<rr:EmailType>{{EmailType}}</rr:EmailType>{{/EmailType}}
|
||||||
|
{{#Preferred}}<rr:Preferred>{{Preferred}}</rr:Preferred>{{/Preferred}}
|
||||||
|
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
|
||||||
|
</rr:Email>
|
||||||
|
{{/Emails}}
|
||||||
|
|
||||||
|
<!-- Driver's License -->
|
||||||
|
{{#DriverLicense}}
|
||||||
|
<rr:DriverLicense>
|
||||||
|
{{#LicenseNumber}}<rr:LicenseNumber>{{LicenseNumber}}</rr:LicenseNumber>{{/LicenseNumber}}
|
||||||
|
{{#LicenseState}}<rr:LicenseState>{{LicenseState}}</rr:LicenseState>{{/LicenseState}}
|
||||||
|
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||||
|
</rr:DriverLicense>
|
||||||
|
{{/DriverLicense}}
|
||||||
|
|
||||||
|
<!-- Insurance -->
|
||||||
|
{{#Insurance}}
|
||||||
|
<rr:Insurance>
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#PolicyNumber}}<rr:PolicyNumber>{{PolicyNumber}}</rr:PolicyNumber>{{/PolicyNumber}}
|
||||||
|
{{#ExpirationDate}}<rr:ExpirationDate>{{ExpirationDate}}</rr:ExpirationDate>{{/ExpirationDate}}
|
||||||
|
{{#ContactName}}<rr:ContactName>{{ContactName}}</rr:ContactName>{{/ContactName}}
|
||||||
|
{{#ContactPhone}}<rr:ContactPhone>{{ContactPhone}}</rr:ContactPhone>{{/ContactPhone}}
|
||||||
|
</rr:Insurance>
|
||||||
|
{{/Insurance}}
|
||||||
|
|
||||||
|
<!-- Linked Accounts -->
|
||||||
|
{{#LinkedAccounts}}
|
||||||
|
<rr:LinkedAccount>
|
||||||
|
<rr:Type>{{Type}}</rr:Type> <!-- FLEET | WARRANTY | CORPORATE -->
|
||||||
|
<rr:AccountNumber>{{AccountNumber}}</rr:AccountNumber>
|
||||||
|
{{#CreditLimit}}<rr:CreditLimit>{{CreditLimit}}</rr:CreditLimit>{{/CreditLimit}}
|
||||||
|
{{#IsDeleted}}<rr:IsDeleted>{{IsDeleted}}</rr:IsDeleted>{{/IsDeleted}}
|
||||||
|
</rr:LinkedAccount>
|
||||||
|
{{/LinkedAccounts}}
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
{{#Notes}}
|
||||||
|
<rr:Notes>
|
||||||
|
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||||
|
</rr:Notes>
|
||||||
|
{{/Notes}}
|
||||||
|
</rr:Customer>
|
||||||
|
</rr:CustomerUpdateRq>
|
||||||
135
server/rr/xml-templates/UpdateRepairOrder.xml
Normal file
135
server/rr/xml-templates/UpdateRepairOrder.xml
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<rr:RepairOrderChgRq xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional request metadata -->
|
||||||
|
{{#RequestId}}<rr:RequestId>{{RequestId}}</rr:RequestId>{{/RequestId}}
|
||||||
|
{{#Environment}}<rr:Environment>{{Environment}}</rr:Environment>{{/Environment}}
|
||||||
|
|
||||||
|
<rr:Dealer>
|
||||||
|
<rr:DealerCode>{{DealerCode}}</rr:DealerCode>
|
||||||
|
{{#DealerName}}<rr:DealerName>{{DealerName}}</rr:DealerName>{{/DealerName}}
|
||||||
|
{{#DealerNumber}}<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>{{/DealerNumber}}
|
||||||
|
{{#StoreNumber}}<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>{{/StoreNumber}}
|
||||||
|
{{#BranchNumber}}<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>{{/BranchNumber}}
|
||||||
|
</rr:Dealer>
|
||||||
|
|
||||||
|
<rr:RepairOrder>
|
||||||
|
<!-- Identity -->
|
||||||
|
{{#RepairOrderId}}<rr:RepairOrderId>{{RepairOrderId}}</rr:RepairOrderId>{{/RepairOrderId}}
|
||||||
|
{{#RepairOrderNumber}}<rr:RepairOrderNumber>{{RepairOrderNumber}}</rr:RepairOrderNumber>{{/RepairOrderNumber}}
|
||||||
|
|
||||||
|
<!-- Header fields that may be patched -->
|
||||||
|
{{#Status}}<rr:Status>
|
||||||
|
{{Status}}</rr:Status>{{/Status}} <!-- e.g., OPEN|IN_PROGRESS|CLOSED -->
|
||||||
|
{{#ROType}}<rr:ROType>
|
||||||
|
{{ROType}}</rr:ROType>{{/ROType}} <!-- e.g., INSURANCE|CUSTOMER_PAY -->
|
||||||
|
{{#OpenDate}}<rr:OpenDate>{{OpenDate}}</rr:OpenDate>{{/OpenDate}}
|
||||||
|
{{#PromisedDate}}<rr:PromisedDate>{{PromisedDate}}</rr:PromisedDate>{{/PromisedDate}}
|
||||||
|
{{#CloseDate}}<rr:CloseDate>{{CloseDate}}</rr:CloseDate>{{/CloseDate}}
|
||||||
|
{{#ServiceAdvisorId}}<rr:ServiceAdvisorId>{{ServiceAdvisorId}}</rr:ServiceAdvisorId>{{/ServiceAdvisorId}}
|
||||||
|
{{#TechnicianId}}<rr:TechnicianId>{{TechnicianId}}</rr:TechnicianId>{{/TechnicianId}}
|
||||||
|
{{#LocationCode}}<rr:LocationCode>{{LocationCode}}</rr:LocationCode>{{/LocationCode}}
|
||||||
|
{{#Department}}<rr:Department>{{Department}}</rr:Department>{{/Department}}
|
||||||
|
{{#PurchaseOrder}}<rr:PurchaseOrder>{{PurchaseOrder}}</rr:PurchaseOrder>{{/PurchaseOrder}}
|
||||||
|
|
||||||
|
<!-- Optional customer patch -->
|
||||||
|
{{#Customer}}
|
||||||
|
<rr:Customer>
|
||||||
|
{{#CustomerId}}<rr:CustomerId>{{CustomerId}}</rr:CustomerId>{{/CustomerId}}
|
||||||
|
{{#CustomerName}}<rr:CustomerName>{{CustomerName}}</rr:CustomerName>{{/CustomerName}}
|
||||||
|
{{#PhoneNumber}}<rr:PhoneNumber>{{PhoneNumber}}</rr:PhoneNumber>{{/PhoneNumber}}
|
||||||
|
{{#EmailAddress}}<rr:EmailAddress>{{EmailAddress}}</rr:EmailAddress>{{/EmailAddress}}
|
||||||
|
</rr:Customer>
|
||||||
|
{{/Customer}}
|
||||||
|
|
||||||
|
<!-- Optional vehicle patch -->
|
||||||
|
{{#Vehicle}}
|
||||||
|
<rr:Vehicle>
|
||||||
|
{{#VIN}}<rr:VIN>{{VIN}}</rr:VIN>{{/VIN}}
|
||||||
|
{{#LicensePlate}}<rr:LicensePlate>{{LicensePlate}}</rr:LicensePlate>{{/LicensePlate}}
|
||||||
|
{{#Year}}<rr:Year>{{Year}}</rr:Year>{{/Year}}
|
||||||
|
{{#Make}}<rr:Make>{{Make}}</rr:Make>{{/Make}}
|
||||||
|
{{#Model}}<rr:Model>{{Model}}</rr:Model>{{/Model}}
|
||||||
|
{{#Odometer}}<rr:Odometer>{{Odometer}}</rr:Odometer>{{/Odometer}}
|
||||||
|
{{#Color}}<rr:Color>{{Color}}</rr:Color>{{/Color}}
|
||||||
|
</rr:Vehicle>
|
||||||
|
{{/Vehicle}}
|
||||||
|
|
||||||
|
<!-- Line changes: use one of AddedJobLines / UpdatedJobLines / RemovedJobLines -->
|
||||||
|
{{#AddedJobLines}}
|
||||||
|
<rr:AddedJobLine>
|
||||||
|
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||||
|
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||||
|
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||||
|
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||||
|
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||||
|
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||||
|
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||||
|
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||||
|
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||||
|
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||||
|
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||||
|
{{#PayType}}<rr:PayType>
|
||||||
|
{{PayType}}</rr:PayType>{{/PayType}} <!-- CUST|INS|WARR|INT -->
|
||||||
|
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||||
|
</rr:AddedJobLine>
|
||||||
|
{{/AddedJobLines}}
|
||||||
|
|
||||||
|
{{#UpdatedJobLines}}
|
||||||
|
<rr:UpdatedJobLine>
|
||||||
|
<!-- Identify the existing line either by Sequence or LineId -->
|
||||||
|
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
||||||
|
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||||
|
{{#ChangeType}}<rr:ChangeType>
|
||||||
|
{{ChangeType}}</rr:ChangeType>{{/ChangeType}} <!-- PRICE|QTY|DESC|OPCODE|PAYTYPE -->
|
||||||
|
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||||
|
{{#Description}}<rr:Description>{{Description}}</rr:Description>{{/Description}}
|
||||||
|
{{#LaborHours}}<rr:LaborHours>{{LaborHours}}</rr:LaborHours>{{/LaborHours}}
|
||||||
|
{{#LaborRate}}<rr:LaborRate>{{LaborRate}}</rr:LaborRate>{{/LaborRate}}
|
||||||
|
{{#PartNumber}}<rr:PartNumber>{{PartNumber}}</rr:PartNumber>{{/PartNumber}}
|
||||||
|
{{#PartDescription}}<rr:PartDescription>{{PartDescription}}</rr:PartDescription>{{/PartDescription}}
|
||||||
|
{{#Quantity}}<rr:Quantity>{{Quantity}}</rr:Quantity>{{/Quantity}}
|
||||||
|
{{#UnitPrice}}<rr:UnitPrice>{{UnitPrice}}</rr:UnitPrice>{{/UnitPrice}}
|
||||||
|
{{#ExtendedPrice}}<rr:ExtendedPrice>{{ExtendedPrice}}</rr:ExtendedPrice>{{/ExtendedPrice}}
|
||||||
|
{{#TaxCode}}<rr:TaxCode>{{TaxCode}}</rr:TaxCode>{{/TaxCode}}
|
||||||
|
{{#PayType}}<rr:PayType>{{PayType}}</rr:PayType>{{/PayType}}
|
||||||
|
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||||
|
</rr:UpdatedJobLine>
|
||||||
|
{{/UpdatedJobLines}}
|
||||||
|
|
||||||
|
{{#RemovedJobLines}}
|
||||||
|
<rr:RemovedJobLine>
|
||||||
|
{{#LineId}}<rr:LineId>{{LineId}}</rr:LineId>{{/LineId}}
|
||||||
|
{{#Sequence}}<rr:Sequence>{{Sequence}}</rr:Sequence>{{/Sequence}}
|
||||||
|
{{#OpCode}}<rr:OpCode>{{OpCode}}</rr:OpCode>{{/OpCode}}
|
||||||
|
{{#Reason}}<rr:Reason>{{Reason}}</rr:Reason>{{/Reason}}
|
||||||
|
</rr:RemovedJobLine>
|
||||||
|
{{/RemovedJobLines}}
|
||||||
|
|
||||||
|
<!-- Totals (optional patch if RR expects header totals on change) -->
|
||||||
|
{{#Totals}}
|
||||||
|
<rr:Totals>
|
||||||
|
{{#LaborTotal}}<rr:LaborTotal>{{LaborTotal}}</rr:LaborTotal>{{/LaborTotal}}
|
||||||
|
{{#PartsTotal}}<rr:PartsTotal>{{PartsTotal}}</rr:PartsTotal>{{/PartsTotal}}
|
||||||
|
{{#MiscTotal}}<rr:MiscTotal>{{MiscTotal}}</rr:MiscTotal>{{/MiscTotal}}
|
||||||
|
{{#TaxTotal}}<rr:TaxTotal>{{TaxTotal}}</rr:TaxTotal>{{/TaxTotal}}
|
||||||
|
{{#GrandTotal}}<rr:GrandTotal>{{GrandTotal}}</rr:GrandTotal>{{/GrandTotal}}
|
||||||
|
</rr:Totals>
|
||||||
|
{{/Totals}}
|
||||||
|
|
||||||
|
<!-- Insurance (optional update) -->
|
||||||
|
{{#Insurance}}
|
||||||
|
<rr:Insurance>
|
||||||
|
{{#CompanyName}}<rr:CompanyName>{{CompanyName}}</rr:CompanyName>{{/CompanyName}}
|
||||||
|
{{#ClaimNumber}}<rr:ClaimNumber>{{ClaimNumber}}</rr:ClaimNumber>{{/ClaimNumber}}
|
||||||
|
{{#AdjusterName}}<rr:AdjusterName>{{AdjusterName}}</rr:AdjusterName>{{/AdjusterName}}
|
||||||
|
{{#AdjusterPhone}}<rr:AdjusterPhone>{{AdjusterPhone}}</rr:AdjusterPhone>{{/AdjusterPhone}}
|
||||||
|
</rr:Insurance>
|
||||||
|
{{/Insurance}}
|
||||||
|
|
||||||
|
<!-- Notes (append or replace depending on RR semantics) -->
|
||||||
|
{{#Notes}}
|
||||||
|
<rr:Notes>
|
||||||
|
{{#Items}}<rr:Note>{{.}}</rr:Note>{{/Items}}
|
||||||
|
</rr:Notes>
|
||||||
|
{{/Notes}}
|
||||||
|
</rr:RepairOrder>
|
||||||
|
</rr:RepairOrderChgRq>
|
||||||
17
server/rr/xml-templates/_EnvelopeFooter.xml
Normal file
17
server/rr/xml-templates/_EnvelopeFooter.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- _EnvelopeFooter.xml -->
|
||||||
|
<rr:Footer xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Optional system trace or session info -->
|
||||||
|
{{#SessionId}}
|
||||||
|
<rr:SessionId>{{SessionId}}</rr:SessionId>
|
||||||
|
{{/SessionId}}
|
||||||
|
|
||||||
|
{{#Checksum}}
|
||||||
|
<rr:Checksum>{{Checksum}}</rr:Checksum>
|
||||||
|
{{/Checksum}}
|
||||||
|
|
||||||
|
{{#Timestamp}}
|
||||||
|
<rr:Timestamp>{{Timestamp}}</rr:Timestamp>
|
||||||
|
{{/Timestamp}}
|
||||||
|
|
||||||
|
<!-- Placeholder for any future required footer elements -->
|
||||||
|
</rr:Footer>
|
||||||
29
server/rr/xml-templates/_EnvelopeHeader.xml
Normal file
29
server/rr/xml-templates/_EnvelopeHeader.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!-- _EnvelopeHeader.xml -->
|
||||||
|
<rr:Authentication xmlns:rr="http://reynoldsandrey.com/">
|
||||||
|
<!-- Required system identifier -->
|
||||||
|
{{#PPSysId}}
|
||||||
|
<rr:PPSysId>{{PPSysId}}</rr:PPSysId>
|
||||||
|
{{/PPSysId}}
|
||||||
|
|
||||||
|
<!-- Dealer / Store / Branch numbers (optional but strongly recommended) -->
|
||||||
|
{{#DealerNumber}}
|
||||||
|
<rr:DealerNumber>{{DealerNumber}}</rr:DealerNumber>
|
||||||
|
{{/DealerNumber}}
|
||||||
|
|
||||||
|
{{#StoreNumber}}
|
||||||
|
<rr:StoreNumber>{{StoreNumber}}</rr:StoreNumber>
|
||||||
|
{{/StoreNumber}}
|
||||||
|
|
||||||
|
{{#BranchNumber}}
|
||||||
|
<rr:BranchNumber>{{BranchNumber}}</rr:BranchNumber>
|
||||||
|
{{/BranchNumber}}
|
||||||
|
|
||||||
|
<!-- Basic user credentials (always required) -->
|
||||||
|
<rr:Username>{{Username}}</rr:Username>
|
||||||
|
<rr:Password>{{Password}}</rr:Password>
|
||||||
|
|
||||||
|
<!-- Optional custom correlation token -->
|
||||||
|
{{#CorrelationId}}
|
||||||
|
<rr:CorrelationId>{{CorrelationId}}</rr:CorrelationId>
|
||||||
|
{{/CorrelationId}}
|
||||||
|
</rr:Authentication>
|
||||||
@@ -416,14 +416,6 @@ const redisSocketEvents = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("rr-get-advisors", async ({ jobid, params }, cb) => {
|
|
||||||
// similar pattern using RrGetAdvisors
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("rr-get-parts", async ({ jobid, params }, cb) => {
|
|
||||||
// similar pattern using RrGetParts
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("rr-get-advisors", async ({ jobid, params }, cb) => {
|
socket.on("rr-get-advisors", async ({ jobid, params }, cb) => {
|
||||||
try {
|
try {
|
||||||
const { RrGetAdvisors } = require("../rr/rr-lookup");
|
const { RrGetAdvisors } = require("../rr/rr-lookup");
|
||||||
|
|||||||
Reference in New Issue
Block a user