feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Checkpoint
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// DmsPostForm updated
|
||||
import { DeleteFilled, DownOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
@@ -270,6 +271,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
/>
|
||||
</Space>
|
||||
|
||||
{dms !== "rr" && (
|
||||
<Form.List name={["payers"]}>
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
@@ -380,6 +382,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
)}
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
@@ -391,15 +394,16 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
});
|
||||
|
||||
// 2) Subtotal from socket.allocationsSummary (existing behavior)
|
||||
const totals =
|
||||
socket.allocationsSummary &&
|
||||
const totals = socket
|
||||
? socket.allocationsSummary &&
|
||||
socket.allocationsSummary.reduce(
|
||||
(acc, val) => ({
|
||||
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||
totalCost: acc.totalCost.add(Dinero(val.cost))
|
||||
}),
|
||||
{ totalSale: Dinero(), totalCost: Dinero() }
|
||||
);
|
||||
)
|
||||
: { totalSale: Dinero(), totalCost: Dinero() };
|
||||
|
||||
const discrep = totals ? totals.totalSale.subtract(totalAllocated) : Dinero();
|
||||
|
||||
@@ -408,8 +412,9 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
|
||||
// Require at least one complete payer row
|
||||
const payersOk =
|
||||
payers.length > 0 &&
|
||||
payers.every((p) => p?.name && p.dms_acctnumber && (p.amount ?? "") !== "" && p.controlnumber);
|
||||
dms === "rr" ||
|
||||
(payers.length > 0 &&
|
||||
payers.every((p) => p?.name && p.dms_acctnumber && (p.amount ?? "") !== "" && p.controlnumber));
|
||||
|
||||
// 4) Disable rules:
|
||||
// - For non-RR: keep the original discrepancy rule (must have summary and zero discrepancy)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// DmsContainer updated
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Col, Result, Row, Select, Space } from "antd";
|
||||
import queryString from "query-string";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -66,6 +67,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
// New unified wss socket (Fortellis, RR)
|
||||
const { socket: wsssocket } = useSocket();
|
||||
|
||||
const activeSocket = useMemo(() => {
|
||||
return dms === "rr" || (dms === "cdk" && Fortellis.treatment === "on") ? wsssocket : socket;
|
||||
}, [dms, Fortellis.treatment, wsssocket, socket]);
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_JOB_EXPORT_DMS, {
|
||||
variables: { id: jobId },
|
||||
skip: !jobId,
|
||||
@@ -229,12 +234,12 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
} ${data.jobs_by_pk.v_make_desc || ""} ${data.jobs_by_pk.v_model_desc || ""}`}
|
||||
</span>
|
||||
}
|
||||
socket={socket}
|
||||
socket={activeSocket}
|
||||
jobId={jobId}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={24} lg={14}>
|
||||
<DmsPostForm socket={socket} jobId={jobId} job={data?.jobs_by_pk} logsRef={logsRef} />
|
||||
<DmsPostForm socket={activeSocket} jobId={jobId} job={data?.jobs_by_pk} logsRef={logsRef} />
|
||||
</Col>
|
||||
|
||||
<DmsCustomerSelector jobid={jobId} />
|
||||
|
||||
658
server/rr/lib/README.md
Normal file
658
server/rr/lib/README.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# rr-rome-client
|
||||
|
||||
A minimal Node.js wrapper for Reynolds & Reynolds (Rome/RIH) STAR `ProcessMessage` over SOAP, with WS-Security UsernameToken.
|
||||
|
||||
## Contents
|
||||
- Overview
|
||||
- Install
|
||||
- Supported Node Version
|
||||
- Peer / External Dependencies
|
||||
- Quick Start
|
||||
- Configuration & Environment Variables
|
||||
- Client API Methods
|
||||
- RRResult Structure
|
||||
- Types & IntelliSense
|
||||
- Errors & Validation
|
||||
- Retry Strategy
|
||||
- Debug / Dump Flags
|
||||
- Live Test Runner (`scripts/run-live.mjs`)
|
||||
- Bundling & Upload Helper (`scripts/bundle-for-upload.mjs`)
|
||||
- XML Templates & XSDs
|
||||
- Logging
|
||||
- Build & Development
|
||||
- Design Notes / Non-goals
|
||||
- License
|
||||
|
||||
## Overview
|
||||
`rr-rome-client` builds and sends STAR XML payloads (Customer, Service Vehicle, Combined Search, Advisors, Parts, BSM Repair Orders) inside a SOAP envelope using the STAR Transport `ProcessMessage` operation. It applies a WS-Security UsernameToken header and provides:
|
||||
- High-level `RRClient` methods for each supported Rome operation.
|
||||
- JSDoc typedefs for all payloads and response shapes (usable in JS and TS).
|
||||
- Structured parsing of response status blocks and operation-specific data.
|
||||
- Automatic BODId & CreationDateTime generation, with ability to override.
|
||||
- Basic input validation and error classification.
|
||||
- Exponential backoff + jitter for retryable transport/vendor lock scenarios.
|
||||
|
||||
## Install
|
||||
```bash
|
||||
npm i rr-rome-client
|
||||
```
|
||||
(You may also need peer dependencies; see below.)
|
||||
|
||||
## Supported Node Version
|
||||
`package.json` declares `"engines": { "node": ">=22.0.0" }`. The build targets modern Node 22 features (native ESM, improved performance). Earlier Node versions are not officially supported/tested.
|
||||
|
||||
## Peer / External Dependencies
|
||||
Rollup externalizes runtime libraries (they are listed in `dependencies` but not bundled). Ensure these are available in your environment:
|
||||
Required:
|
||||
- axios – HTTP transport
|
||||
- fast-xml-parser – STAR XML parsing
|
||||
- mustache – XML templating
|
||||
- uuid – BODId generation
|
||||
Optional (only if you use env loader helpers or live scripts):
|
||||
- dotenv
|
||||
- dotenv-expand
|
||||
|
||||
Install (versions per `package.json`):
|
||||
```bash
|
||||
npm install axios@^1.7.7 fast-xml-parser@^4.5.0 mustache@^4.2.0 uuid@^9.0.1
|
||||
# Optional env loader
|
||||
npm install dotenv@^17.2.3 dotenv-expand@^12.0.3
|
||||
```
|
||||
TypeScript users (optional):
|
||||
```bash
|
||||
npm install -D @types/node
|
||||
```
|
||||
Browser bundling is not officially supported; you would need polyfills for core modules if attempting.
|
||||
|
||||
## Quick Start
|
||||
```js
|
||||
import { RRClient } from 'rr-rome-client';
|
||||
import { loadEnv } from 'rr-rome-client/src/util/config.js'; // optional helper
|
||||
|
||||
// Load routing & credentials from environment variables (see below)
|
||||
const { baseUrl, username, password, routing } = loadEnv();
|
||||
|
||||
const client = new RRClient({ baseUrl, username, password });
|
||||
|
||||
// Minimal createRepairOrder example
|
||||
const result = await client.createRepairOrder({
|
||||
customerNo: '12345',
|
||||
departmentType: 'S', // DeptType
|
||||
vin: '1ABCDEF2GHIJ34567',
|
||||
outsdRoNo: 'EXT-RO-99'
|
||||
}, { routing });
|
||||
|
||||
if (result.success) {
|
||||
console.log('RO status:', result.data); // {status, date, time, outsdRoNo, dmsRoNo, errorMessage}
|
||||
} else {
|
||||
console.error('Failure:', result.statusBlocks?.transaction);
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration & Environment Variables
|
||||
You can manually provide configuration or use the helper `loadEnv(env)` in `src/util/config.js`.
|
||||
|
||||
Recognized environment variables:
|
||||
- RR_BASE_URL – SOAP endpoint URL (required)
|
||||
- RR_USERNAME – WS-Security UsernameToken username (required)
|
||||
- RR_PASSWORD – WS-Security UsernameToken password (required)
|
||||
- RR_DEALER_NUMBER – DealerNumber for Destination (required per call)
|
||||
- RR_STORE_NUMBER – StoreNumber (optional)
|
||||
- RR_AREANUMBER – AreaNumber (optional)
|
||||
|
||||
Example `.env`:
|
||||
```
|
||||
RR_BASE_URL=https://rome.example.com/soap
|
||||
RR_USERNAME=integratorUser
|
||||
RR_PASSWORD=superSecret
|
||||
RR_DEALER_NUMBER=1234
|
||||
RR_STORE_NUMBER=01
|
||||
RR_AREANUMBER=1
|
||||
```
|
||||
`loadEnv()` returns `{ baseUrl, username, password, routing: { dealerNumber, storeNumber, areaNumber } }`.
|
||||
|
||||
Per-call options object shape (`CallOptions`):
|
||||
```js
|
||||
{
|
||||
routing: { dealerNumber: '1234', storeNumber?: '01', areaNumber?: '1' },
|
||||
envelope?: { bodId?, creationDateTime?, sender?: { component?, task?, referenceId? } }
|
||||
}
|
||||
```
|
||||
If omitted, `RRClient` auto-generates `bodId` (UUID) and `creationDateTime` when sending.
|
||||
|
||||
## Client API Methods
|
||||
Each method returns a `Promise<RRResult<T>>` where `T` is operation-specific data (or array). `success` is true for vendor SUCCESS and NO_MATCH results; FAIL triggers an `RRVendorStatusError` exception before a `RRResult` is returned.
|
||||
|
||||
All methods require `opts.routing.dealerNumber`.
|
||||
|
||||
### combinedSearch(payload: CombinedSearchQuery, opts)
|
||||
Search customer + service vehicle combinations by exactly one criterion: `phone | license | vin | name | nameRecId | stkNo` plus optional `make`, `model`, `year`, `maxResults` (capped at 50).
|
||||
Minimal:
|
||||
```js
|
||||
const res = await client.combinedSearch({ kind: 'vin', vin: '12345' }, { routing });
|
||||
res.data; // Array<CombinedSearchBlock>
|
||||
```
|
||||
Errors: Throws `RRValidationError` if missing or multiple criteria.
|
||||
|
||||
### insertCustomer(payload: InsertCustomerPayload, opts)
|
||||
Insert a customer record. Required: `lastName` (or `customerName`). If individual (`ibFlag='I'` or inferred by presence of `firstName`), then `firstName` required.
|
||||
```js
|
||||
const res = await client.insertCustomer({ firstName: 'Jane', lastName: 'Doe', phones:[{number:'5551234567'}] }, { routing });
|
||||
res.data; // { dmsRecKey, status, statusCode }
|
||||
```
|
||||
|
||||
### updateCustomer(payload: UpdateCustomerPayload, opts)
|
||||
Update existing customer by `nameRecId` plus required `ibFlag`. Other fields optional.
|
||||
```js
|
||||
const res = await client.updateCustomer({ nameRecId:'998877', ibFlag:'I', lastName:'Doe' }, { routing });
|
||||
```
|
||||
|
||||
### insertServiceVehicle(payload: InsertServiceVehiclePayload, opts)
|
||||
Insert a service vehicle linked to a customer. Required: `vin`, `vehicleServInfo.customerNo`.
|
||||
```js
|
||||
const res = await client.insertServiceVehicle({
|
||||
vin:'1HGCM82633A004352',
|
||||
vehicleServInfo:{ customerNo:'12345' }
|
||||
}, { routing });
|
||||
res.data; // { status, statusCode }
|
||||
```
|
||||
|
||||
### getAdvisors(payload: GetAdvisorsParams, opts)
|
||||
Fetch advisors for a department. Department values normalized: S/P/B or long names.
|
||||
```js
|
||||
const res = await client.getAdvisors({ department:'SERVICE' }, { routing });
|
||||
res.data; // AdvisorRow[]
|
||||
```
|
||||
|
||||
### createRepairOrder(payload: CreateRepairOrderPayload, opts)
|
||||
Required: `customerNo`, `departmentType`, `vin`, `outsdRoNo`. Advisor optional. Complex nested labor/parts/misc blocks supported via payload.
|
||||
```js
|
||||
const res = await client.createRepairOrder({
|
||||
customerNo:'12345', departmentType:'S', vin:'1ABCDEF2GHIJ34567', outsdRoNo:'EXT-RO-99'
|
||||
}, { routing });
|
||||
res.data; // { status, date, time, outsdRoNo, dmsRoNo, errorMessage }
|
||||
```
|
||||
|
||||
### updateRepairOrder(payload: UpdateRepairOrderPayload, opts)
|
||||
Required: `finalUpdate ('Y'|'N')`, `outsdRoNo`. May include `roNo` and nested sections.
|
||||
```js
|
||||
const res = await client.updateRepairOrder({ finalUpdate:'N', outsdRoNo:'EXT-RO-99' }, { routing });
|
||||
```
|
||||
|
||||
### getParts(payload: GetPartsParams, opts)
|
||||
Required: `roNumber` (internal ERA RO number).
|
||||
```js
|
||||
const res = await client.getParts({ roNumber:'938275' }, { routing });
|
||||
res.data; // PartRow[]
|
||||
```
|
||||
|
||||
## Payload Schema Reference
|
||||
Comprehensive field-level summary sourced from `src/types.js`, operation builders, and validation logic. Types reflect accepted JS types (string|number where applicable). Constraints list enumerations, inference rules, and validation notes. Required = must be supplied by caller (or inferred automatically). Optional fields omitted become absent in generated XML.
|
||||
|
||||
### CombinedSearchQuery
|
||||
Only one criterion permitted; `maxResults` capped at 50.
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| kind | 'phone'|'license'|'vin'|'name'|'nameRecId'|'stkNo' | Yes | Determines which single criterion block is emitted |
|
||||
| phone | string\|number\|{phone:string} | Conditionally (if kind==='phone') | Value mapped to `<Phone Num="..."/>` |
|
||||
| license | string\|number\|{license:string} | Conditionally (kind==='license') | Value mapped to `<LicenseNum LicNo="..."/>` |
|
||||
| vin | string\|number\|{vin:string} | Conditionally (kind==='vin') | Value mapped to `<PartVIN Vin="..."/>` (partial VIN allowed) |
|
||||
| name | {fname,lname,mname} or {name} | Conditionally (kind==='name') | Either FullName triple or LName only; must supply all three for FullName |
|
||||
| nameRecId | string\|number\|{custId:string}|{nameRecId:string} | Conditionally (kind==='nameRecId') | Emits `<NameRecId CustIdStart="..."/>` |
|
||||
| stkNo | string\|number\|{stkNo:string} | Conditionally (kind==='stkNo') | Emits `<StkNo VehId="..."/>` |
|
||||
| maxResults | number | No | Capped at 50 (default 50) -> `MaxRecs` attribute |
|
||||
| make | string | No | Defaults 'ANY' -> `VehData MakePfx` |
|
||||
| model | string\|number | No | Defaults 'ANY' -> `VehData Model` |
|
||||
| year | string\|number | No | Defaults 'ANY' -> `VehData Year` |
|
||||
|
||||
### InsertCustomerPayload
|
||||
`ibFlag` inferred as 'I' if `firstName` present, else 'B'. Business requires `lastName` / `customerName`; individual requires `firstName` + last name.
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| ibFlag | 'I'|'B' | Auto / For update must supply | Inferred if omitted (firstName present => 'I') |
|
||||
| customerType | 'R'|'W'|'I'|'Retail'|'Wholesale'|'Internal' | No | Normalized to 'R','W','I'; must be one of listed |
|
||||
| createdBy | string | No | Optional CreatedBy attribute |
|
||||
| customerName | string | Conditional | Alias for `lastName` when business |
|
||||
| lastName | string | Yes (unless `customerName` provided) | Required base name; sanitized to A-Z0-9 space |
|
||||
| firstName | string | Required when ibFlag='I' | Sanitized; required for individuals |
|
||||
| midName | string | No | Sanitized |
|
||||
| salut | string | No | Sanitized |
|
||||
| suffix | string | No | Sanitized |
|
||||
| addresses | CustomerAddress[] | No | Each entry requires `line1`; Type defaults 'P' |
|
||||
| phones | CustomerPhone[] | No | Each entry requires `number`; Type defaults 'H' |
|
||||
| emails | CustomerEmail[] | No | First entry used -> `<Email MailTo="..."/>` |
|
||||
| personal.gender | 'M'|'F'|'U' | No | Optional |
|
||||
| personal.otherName | string | No | Sanitized alnum/space |
|
||||
| personal.anniversaryDate | string | No | Included if non-empty |
|
||||
| personal.employerName | string | No | Sanitized |
|
||||
| personal.employerPhone | string | No | Raw string |
|
||||
| personal.occupation | string | No | Sanitized |
|
||||
| personal.optOut | string | No | Pass-through |
|
||||
| personal.optOutUse | string | No | Pass-through |
|
||||
| personal.birthDates[].type | 'P'|'S' | No | Defaults 'P'; entry must have `date` |
|
||||
| personal.birthDates[].date | string | Conditional | Included only if non-empty |
|
||||
| personal.ssns[].type | 'P'|'S' | No | Defaults 'P'; entry must have `ssn` |
|
||||
| personal.ssns[].ssn | string | Conditional | Included only if non-empty |
|
||||
| personal.driver.type | 'P'|'S' | No | Defaults 'P' |
|
||||
| personal.driver.licenseNumber | string | Conditional | Required to emit DriverInfo |
|
||||
| personal.driver.licenseState | string | No | Optional |
|
||||
| personal.driver.licenseExpDate | string | No | Optional |
|
||||
| personal.children[].name | string | No | Sanitized; optional list |
|
||||
| dms.taxExemptNum | string | No | Optional |
|
||||
| dms.salesTerritory | string | No | Optional |
|
||||
| dms.deliveryRoute | string | No | Optional |
|
||||
| dms.salesmanNum | string | No | Optional |
|
||||
| dms.lastContactMethod | string | No | Optional |
|
||||
| dms.followups[].type | string | Conditional | Must have both type & value |
|
||||
| dms.followups[].value | string | Conditional | Must have both type & value |
|
||||
|
||||
### UpdateCustomerPayload
|
||||
Extends InsertCustomerPayload plus:
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|-------|------|----------|-------------|
|
||||
| nameRecId | string\|number | Yes | Required identifier |
|
||||
| ibFlag | 'I'|'B' | Yes | Must be explicitly provided on update |
|
||||
|
||||
### InsertServiceVehiclePayload
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| vin | string | Yes | Must be provided |
|
||||
| modelDesc | string | No | Optional attribute |
|
||||
| carline | string | No | Optional attribute |
|
||||
| extClrDesc | string | No | Optional attribute |
|
||||
| intClrDesc | string | No | Optional attribute |
|
||||
| trimDesc | string | No | Optional attribute |
|
||||
| bodyStyle | string | No | Optional attribute |
|
||||
| engineDesc | string | No | Optional attribute |
|
||||
| transDesc | string | No | Optional attribute |
|
||||
| year | string\|number | No | Emits `<Year>` element if present |
|
||||
| odometer | string\|number | No | Emits `<Odometer>` |
|
||||
| odometerUnits | string | No | Emits `<OdometerUnits>` |
|
||||
| vehicleDetail.licNo | string | No | Emits `<VehicleDetail LicNo="..."/>` |
|
||||
| vehicleServInfo.customerNo | string\|number | Yes | Required; becomes `CustomerNo` attribute |
|
||||
| vehicleServInfo.salesmanNo | string\|number | No | Optional element |
|
||||
| vehicleServInfo.inServiceDate | string\|number | No | Optional element |
|
||||
| vehicleServInfo.mileage | string\|number | No | Optional element |
|
||||
| vehicleServInfo.teamCode | string | No | Optional element |
|
||||
| vehicleServInfo.vehExtWarranty.contractNumber | string | Conditional | Included if any warranty field present |
|
||||
| vehicleServInfo.vehExtWarranty.expirationDate | string | Conditional | "" excluded |
|
||||
| vehicleServInfo.vehExtWarranty.expirationMileage | string\|number | Conditional | "" excluded |
|
||||
| vehicleServInfo.advisor.contactInfo.nameRecId | string\|number | No | Advisor block included only if provided |
|
||||
|
||||
### CreateRepairOrderPayload
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| customerNo | string\|number | Yes | CustNo |
|
||||
| departmentType | string\|number | Yes | DeptType |
|
||||
| vin | string | Yes | Vin |
|
||||
| outsdRoNo | string\|number | Yes | External RO identifier |
|
||||
| advisorNo | string\|number | No | AdvNo |
|
||||
| tagNo | string\|number | No | TagNo |
|
||||
| mileageIn | string\|number | No | MileageIn |
|
||||
| roComment | string | No | `<RoCommentInfo RoComment="..."/>` |
|
||||
| estimate.parts | string\|number | No | EstPartsAmt |
|
||||
| estimate.labor | string\|number | No | EstLaborAmt |
|
||||
| estimate.total | string\|number | No | EstTotalAmt |
|
||||
| tax.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration validated |
|
||||
| tax.taxCode | string | No | TaxCode |
|
||||
| tax.txblGrossAmt | string\|number | No | TxblGrossAmt |
|
||||
| tax.grossTaxAmt | string\|number | No | GrossTaxAmt |
|
||||
| rolabor.ops[].opCode | string | No | Optional |
|
||||
| rolabor.ops[].jobNo | string\|number | No | Optional |
|
||||
| rolabor.ops[].custPayTypeFlag | string | No | Freeform (not validated) |
|
||||
| rolabor.ops[].warrPayTypeFlag | string | No | Freeform |
|
||||
| rolabor.ops[].intrPayTypeFlag | string | No | Freeform |
|
||||
| rolabor.ops[].custTxblNtxblFlag | 'T'|'N' | No | Enumerated & validated |
|
||||
| rolabor.ops[].warrTxblNtxblFlag | 'T'|'N' | No | Enumerated & validated |
|
||||
| rolabor.ops[].intrTxblNtxblFlag | 'T'|'N' | No | Enumerated & validated |
|
||||
| rolabor.ops[].vlrCode | string | No | Optional |
|
||||
| rolabor.ops[].bill.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration |
|
||||
| rolabor.ops[].bill.jobTotalHrs | string\|number | No | Optional |
|
||||
| rolabor.ops[].bill.billTime | string\|number | No | Optional |
|
||||
| rolabor.ops[].bill.billRate | string\|number | No | Optional |
|
||||
| rolabor.ops[].ccc.cause | string | No | Optional |
|
||||
| rolabor.ops[].ccc.complaint | string | No | Optional |
|
||||
| rolabor.ops[].ccc.correction | string | No | Optional |
|
||||
| rolabor.ops[].amount.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration |
|
||||
| rolabor.ops[].amount.amtType | string | No | Optional |
|
||||
| rolabor.ops[].amount.custPrice | string\|number | No | Optional |
|
||||
| rolabor.ops[].amount.totalAmt | string\|number | No | Optional |
|
||||
| ropart.jobs[].opCode | string | No | Optional |
|
||||
| ropart.jobs[].jobNo | string\|number | No | Optional |
|
||||
| ropart.jobs[].lines[].partNo | string | No | Optional |
|
||||
| ropart.jobs[].lines[].partNoDesc | string | No | Optional |
|
||||
| ropart.jobs[].lines[].partQty | string\|number | No | Emits QtyOrd |
|
||||
| ropart.jobs[].lines[].sale | string\|number | No | Sale |
|
||||
| ropart.jobs[].lines[].cost | string\|number | No | Cost |
|
||||
| ropart.jobs[].lines[].addDeleteFlag | string | No | AddDeleteFlag |
|
||||
| rogg.ops[].lines[].breakOut | string | No | Optional |
|
||||
| rogg.ops[].lines[].itemType | 'G'|'P'|'S'|'F' | No | Enumerated & validated |
|
||||
| rogg.ops[].lines[].itemDesc | string | No | Optional |
|
||||
| rogg.ops[].lines[].custQty | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].warrQty | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].intrQty | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].custPayTypeFlag | string | No | Optional |
|
||||
| rogg.ops[].lines[].warrPayTypeFlag | string | No | Optional |
|
||||
| rogg.ops[].lines[].intrPayTypeFlag | string | No | Optional |
|
||||
| rogg.ops[].lines[].custTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| rogg.ops[].lines[].warrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| rogg.ops[].lines[].intrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| rogg.ops[].lines[].amount.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration |
|
||||
| rogg.ops[].lines[].amount.amtType | string | No | Optional |
|
||||
| rogg.ops[].lines[].amount.custPrice | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].amount.dlrCost | string\|number | No | Optional |
|
||||
| romisc.ops[].lines[].miscCode | string | No | Optional |
|
||||
| romisc.ops[].lines[].custPayTypeFlag | string | No | Optional |
|
||||
| romisc.ops[].lines[].warrPayTypeFlag | string | No | Optional |
|
||||
| romisc.ops[].lines[].intrPayTypeFlag | string | No | Optional |
|
||||
| romisc.ops[].lines[].custTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| romisc.ops[].lines[].warrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| romisc.ops[].lines[].intrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| romisc.ops[].lines[].codeAmt | string\|number | No | Optional |
|
||||
|
||||
### UpdateRepairOrderPayload
|
||||
Adds required `finalUpdate` and supports most Create fields + mileageOut.
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| finalUpdate | 'Y'|'N' | Yes | Must be 'Y' or 'N' (validated) |
|
||||
| outsdRoNo | string\|number | Yes | External RO identifier (validation requires present) |
|
||||
| roNo | string\|number | No | Optional internal RoNo |
|
||||
| mileageOut | string\|number | No | MileageOut |
|
||||
| (other fields) | see CreateRepairOrderPayload | Conditional | Same validations apply |
|
||||
|
||||
### GetAdvisorsParams
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| department | 'S'|'P'|'B'|'SERVICE'|'PARTS'|'BODY'|'BODYSHOP'|'BODY SHOP' | Yes | Normalized to S/P/B |
|
||||
| advisorNumber | string\|number | No | Optional filter |
|
||||
| maxResults | number | Ignored | Not in XSD (builder ignores) |
|
||||
|
||||
### GetPartsParams
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| roNumber | string\|number | Yes | Required; becomes `<RoInfo RoNumber="..."/>` |
|
||||
|
||||
### Common Routing & Envelope Fields
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| routing.dealerNumber | string | Yes | Required for all requests (Destination) |
|
||||
| routing.storeNumber | string | No | Optional Destination StoreNumber |
|
||||
| routing.areaNumber | string | No | Optional Destination AreaNumber |
|
||||
| envelope.bodId | string | Auto | UUID generated if omitted |
|
||||
| envelope.creationDateTime | Date\|string | Auto | Uses current time; formatted without milliseconds |
|
||||
| envelope.sender.component | string | No | Defaults 'Rome' if omitted |
|
||||
| envelope.sender.task | string | No | Op-specific defaults (e.g. 'CU','SV','BSMRO','CVC','RCT') |
|
||||
| envelope.sender.referenceId | string | No | Op-specific defaults ('Insert','Update','Query') |
|
||||
|
||||
### AdvisorRow (response convenience)
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| advisorId | string\|number\|undefined | AdvisorNumber attribute |
|
||||
| firstName | string\|undefined | FirstName attribute |
|
||||
| lastName | string\|undefined | LastName attribute |
|
||||
| department | 'S'|'P'|'B'|undefined | Normalized department passed through |
|
||||
|
||||
### PartRow (response convenience)
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| partNumber | string\|undefined | PartNumber |
|
||||
| partDescription | string\|undefined | PartDescription |
|
||||
| quantityOrdered | string\|number\|undefined | QuantityOrdered |
|
||||
| quantityShipped | string\|number\|undefined | QuantityShipped |
|
||||
| price | string\|number\|undefined | Price |
|
||||
| cost | string\|number\|undefined | Cost |
|
||||
| processedFlag | string\|undefined | ProcessedFlag |
|
||||
| addOrDelete | string\|undefined | AddOrDelete |
|
||||
|
||||
### CombinedSearch Response Shapes
|
||||
Hierarchical breakdown of the parsed array returned under `RRResult.data` for `combinedSearch` (each entry is a `CombinedSearchBlock`). Attribute-centric; absent fields are simply omitted.
|
||||
|
||||
#### CombinedSearchBlock
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| NameContactId | CombinedSearchNameContactId | Customer name/contact composite |
|
||||
| ServVehicle | CombinedSearchServVehicle[] | Zero or more service vehicle blocks |
|
||||
| Message | CombinedSearchMessage[] | Optional informational messages |
|
||||
|
||||
#### NameContactId
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| NameId | CombinedSearchNameId | Includes identifiers & individual/business name choice |
|
||||
| Address | Object[] | Each has address-related attributes (lines, city, state, zip, etc.) |
|
||||
| ContactOptions | Object[] | Arbitrary contact option attributes as present |
|
||||
| Phone | Object[] | Each phone entry carries attributes like Type, Num, Ext |
|
||||
| Email | Object[] | Each email entry carries attributes (MailTo, etc.) |
|
||||
|
||||
#### NameId
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| NameRecId | string\|number | Identifier used for subsequent operations |
|
||||
| IBFlag | 'I'|'B' | Individual or Business flag |
|
||||
| IndName | Object | Present for individual: attributes like FName, LName, MName |
|
||||
| BusName | Object | Present for business: attributes like Name (business name) |
|
||||
|
||||
#### ServVehicle (CombinedSearchServVehicle)
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| Vehicle | CombinedSearchVehicle | VIN + descriptive attributes |
|
||||
| VehicleServInfo | CombinedSearchVehicleServInfo | Service info attributes & nested warranty/advisor/comments |
|
||||
|
||||
#### Vehicle (CombinedSearchVehicle)
|
||||
| Attribute | Type | Notes |
|
||||
|-----------|------|-------|
|
||||
| Vin | string | Vehicle identification number (may be partial in search results) |
|
||||
| VehicleMake | string | Make code / description |
|
||||
| VehicleYr | string\|number | Year |
|
||||
| MdlNo | string | Model code |
|
||||
| ModelDesc | string | Model description |
|
||||
| Carline | string | Carline description |
|
||||
| ExtClrDesc | string | Exterior color |
|
||||
| IntClrDesc | string | Interior color |
|
||||
| MakeName | string | Full make name |
|
||||
| VehicleDetail.LicNo | string | License plate (if present) |
|
||||
|
||||
#### VehicleServInfo (CombinedSearchVehicleServInfo)
|
||||
| Attribute | Type | Notes |
|
||||
|-----------|------|-------|
|
||||
| CustomerNo | string\|number | Linked customer number |
|
||||
| SalesmanNo | string\|number | Optional salesman number |
|
||||
| InServiceDate | string\|number | Date vehicle placed in service |
|
||||
| Mileage | string\|number | Current mileage |
|
||||
| TeamCode | string | Team/department code |
|
||||
| VehExtWarranty.ContractNumber | string | Extended warranty contract number |
|
||||
| VehExtWarranty.ExpirationDate | string | Warranty expiration date |
|
||||
| VehExtWarranty.ExpirationMileage | string\|number | Warranty mileage limit |
|
||||
| Advisor.ContactInfo.NameRecId | string\|number | Advisor reference (if present) |
|
||||
| VehServComments[] | string[] | Freeform service comments (array of raw text) |
|
||||
|
||||
#### Message (CombinedSearchMessage)
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| MessageNo | string\|number | Optional message number (if provided) |
|
||||
| Text | string | Message text content |
|
||||
|
||||
### CustomerResponseData
|
||||
Returned as `result.data` for `insertCustomer` / `updateCustomer`.
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| dmsRecKey | string\|undefined | DMS record key identifier (if provided in TransStatus) |
|
||||
| status | string\|undefined | Vendor status string |
|
||||
| statusCode | string\|undefined | Vendor status code |
|
||||
|
||||
### ServiceVehicleResponseData
|
||||
Returned as `result.data` for `insertServiceVehicle`.
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| status | string\|undefined | GenTransStatus Status |
|
||||
| statusCode | string\|undefined | GenTransStatus StatusCode |
|
||||
|
||||
### RepairOrderData
|
||||
Returned as `result.data` for `createRepairOrder` / `updateRepairOrder` (parsed from RoRecordStatus).
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| status | string\|undefined | RoRecordStatus Status |
|
||||
| date | string\|undefined | Date attribute/text |
|
||||
| time | string\|undefined | Time attribute/text |
|
||||
| outsdRoNo | string\|undefined | External RO number (OutsdRoNo) |
|
||||
| dmsRoNo | string\|undefined | Internal DMS RO number (DMSRoNo) |
|
||||
| errorMessage | string\|undefined | ErrorMessage if provided |
|
||||
|
||||
## RRResult Structure
|
||||
Each successful call resolves to:
|
||||
```ts
|
||||
interface RRResult<T> {
|
||||
success: boolean; // SUCCESS or NO_MATCH
|
||||
data?: T; // op-specific parsed convenience data
|
||||
parsed: any; // entire parsed STAR payload root
|
||||
xml: { request: string; response: string }; // raw SOAP envelopes
|
||||
statusBlocks?: { transaction?: {status,statusCode,message}; roRecord?: {status,date,time,outsdRoNo,dmsRoNo,errorMessage} };
|
||||
applicationArea?: any; // raw ApplicationArea node
|
||||
}
|
||||
```
|
||||
Use `statusBlocks.transaction` for generic status; `data` for normalized op output.
|
||||
|
||||
`Convenience data` refers to the distilled, operation-specific subset placed on `RRResult.data` by a dedicated `postParse` function (e.g., extracting only RoRecordStatus identifiers or customer DMS keys). It is intentionally smaller and flatter than `RRResult.parsed`, which contains the entire parsed STAR payload tree. Use `data` for common identifiers/status checks; fall back to `parsed` when you need full raw XML-derived detail.
|
||||
|
||||
## Types & IntelliSense
|
||||
Rich JSDoc typedefs ship with the package.
|
||||
- ESM: `import { RRClient } from 'rr-rome-client';` optionally import `'rr-rome-client/types'` to prompt editor indexing.
|
||||
- CJS: `const { RRClient } = require('rr-rome-client'); require('rr-rome-client/types');`
|
||||
|
||||
Selected typedef categories (see `src/types.js`):
|
||||
- Routing / Envelope / CallOptions
|
||||
- Customer / Service Vehicle / Repair Order payload blocks
|
||||
- Combined Search structures
|
||||
- Advisor and Parts row shapes
|
||||
- `RRResult<T>` generic helper
|
||||
|
||||
The build also emits a TypeScript declaration bundle (`dist/types/index.d.ts`) generated via `tsconfig.types.json` (processing only `src/types.js`).
|
||||
|
||||
## Errors & Validation
|
||||
Three custom error classes (`src/errors.js`):
|
||||
- `RRTransportError` – Non-2xx HTTP status or network failure; `meta.status` / `meta.body` may be attached.
|
||||
- `RRVendorStatusError` – Vendor FAIL status (non-success & not NO_MATCH). Includes `meta.status` (raw status object) and full response XML. `retryable` may be set (currently determined by vendor message lock wording or explicit flag).
|
||||
- `RRValidationError` – Input validation failures (missing required fields, invalid enumeration values, etc.).
|
||||
|
||||
Enumerated validations (examples):
|
||||
- Customer: `ibFlag`, required `firstName` if individual, allowed `customerType` values.
|
||||
- Service Vehicle: mandatory `vin` and `vehicleServInfo.customerNo`.
|
||||
- Repair Orders: required header fields; enumerations for tax pay type (`All|Cust|Intr|Warr`), taxable flags (`T|N`), item types (`G|P|S|F`).
|
||||
|
||||
Error Handling Example:
|
||||
```js
|
||||
try {
|
||||
await client.createRepairOrder(/* ... */);
|
||||
} catch (e) {
|
||||
if (e instanceof RRVendorStatusError) {
|
||||
console.error('Vendor fail', e.meta.status);
|
||||
} else if (e instanceof RRValidationError) {
|
||||
console.error('Bad input', e.message);
|
||||
} else if (e instanceof RRTransportError) {
|
||||
console.error('HTTP/Network', e.message, e.meta.status);
|
||||
} else {
|
||||
console.error('Unexpected', e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Retry Strategy
|
||||
`withBackoff(fn, {max, logger})` (used internally) retries on:
|
||||
- Transport/network errors (`RRTransportError`).
|
||||
- Vendor status errors indicating record lock/in use (message matching `/lock|in use|record.*busy/i`) or explicit `retryable` flag.
|
||||
Backoff: exponential starting at 400ms, capped at 10s, plus up to 250ms jitter.
|
||||
Configure via `new RRClient({ retries: { max: 5 }, ... })`.
|
||||
|
||||
## Debug / Dump Flags
|
||||
Set these environment variables to inspect request/response internals:
|
||||
- `RR_DEBUG` – Enables debug logging (`defaultLogger.debug`).
|
||||
- `RR_DUMP_ENVELOPE=1` – Prints outgoing SOAP envelope.
|
||||
- `RR_DUMP_XML=1` – Prints full response XML.
|
||||
- `RR_DUMP_STATUS=1` – Logs parsed status blocks.
|
||||
- `RR_DUMP_APPLICATION=1` – Logs raw ApplicationArea block.
|
||||
- `RR_DUMP_DATA=1` – Logs `result.data` (postParse convenience output).
|
||||
|
||||
## Live Test Runner (`scripts/run-live.mjs`)
|
||||
Provides curated integration tests against a live Rome system using Vitest. Loads `.env` then `.env.local` (override). Usage:
|
||||
```
|
||||
node scripts/run-live.mjs <test> [flags] [-- ...vitestArgs]
|
||||
```
|
||||
Available tests (see script for full list & flags):
|
||||
- `combinedSearch` – VIN or phone search.
|
||||
- `insertCustomer`, `updateCustomer` – Customer record operations (use `--write` to enable live writes).
|
||||
- `insertServiceVehicle` – Add vehicle (requires or discovers customer).
|
||||
- `getAdvisors` – Advisor listing by department.
|
||||
- `createRepairOrder`, `updateRepairOrder` – BSM RO operations.
|
||||
- `getParts` – Retrieve RO parts lines.
|
||||
- `all` – Run all sequentially.
|
||||
|
||||
Common flags:
|
||||
- `--dump` → `RR_DUMP_ENVELOPE`
|
||||
- `--write` → `RR_LIVE_WRITES` (enables writes)
|
||||
- Operation-specific flags map to `RR_TEST_*` env variables (see script comments for details).
|
||||
- Arbitrary env: `--set=KEY=VALUE`.
|
||||
|
||||
Example:
|
||||
```
|
||||
node scripts/run-live.mjs createRepairOrder --write --dump --customerNo=1134485 --vin=1ABCDEF2GHIJ34567 --ro=BSM123
|
||||
```
|
||||
|
||||
## Bundling & Upload Helper (`scripts/bundle-for-upload.mjs`)
|
||||
Creates text bundles of project files for transport/support purposes.
|
||||
- Includes key directories (`src/`, `test/`, `schemas/`) and specific root files.
|
||||
- Excludes large/irrelevant directories (`node_modules`, `dist`, etc.).
|
||||
- Options: `--max-bytes`, `--pattern=globish`, `--with-env` (include `.env` – caution), `--list-only`.
|
||||
Generates `bundles/bundle-<uuid>-N.txt` with file boundary markers.
|
||||
|
||||
## XML Templates & XSDs
|
||||
Templates reside in `src/templates/templateMap.js` using Mustache. Each operation builder renders a STAR root element plus `ApplicationArea`. XSD files (under `schemas/`) accompany operations:
|
||||
- Customer Insert/Update: `rey_RomeCustomerInsertReq.xsd`, `rey_RomeCustomerUpdateReq.xsd`
|
||||
- Service Vehicle Insert: `rey_RomeServVehicleInsertReq.xsd`
|
||||
- Combined Search: `rey_RomeCustServVehCombReq.xsd`
|
||||
- Advisors: `rey_RomeGetAdvisorsReq.xsd`
|
||||
- Parts: `rey_RomeGetPartsReq.xsd`
|
||||
- Repair Orders Create/Update: `rey_RomeCreateBSMRepairOrderReq.xsd`, `rey_RomeUpdateBSMRepairOrderReq.xsd`
|
||||
Response XSDs also present for repair orders and others.
|
||||
|
||||
Builders attach an `xsdFilename` hint and `elementName`; no runtime XSD validation is performed (the project does not contain a validation module beyond these references).
|
||||
|
||||
## Logging
|
||||
Default logger (`src/logger.js`): logs to console via `info`, `warn`, `error`; `debug` gated by `RR_DEBUG`.
|
||||
Provide a custom logger with matching method names in `RRClientConfig`:
|
||||
```js
|
||||
const logger = { info:()=>{}, warn:()=>{}, error:console.error, debug:()=>{} };
|
||||
const client = new RRClient({ baseUrl, username, password, logger });
|
||||
```
|
||||
|
||||
## Build & Development
|
||||
Scripts:
|
||||
- `npm run build` – Rollup builds `dist/index.cjs` & `dist/index.mjs`, copies JSDoc types (`scripts/postbuild-copy-types.mjs`), emits `.d.ts` (via `tsc -p tsconfig.types.json`).
|
||||
- `npm test` – Runs unit tests (`vitest` with `vitest.config.unit.mjs`).
|
||||
- `npm run bundle` – Invoke bundle creation (see above).
|
||||
- `npm run live:<op>` – Convenience commands mapping to `scripts/run-live.mjs` (e.g. `npm run live:getParts`).
|
||||
|
||||
Rollup configuration (`rollup.config.mjs`):
|
||||
- Externalizes Node core modules and listed dependencies.
|
||||
- Applies terser minification (2 passes, hoisting) for compact output.
|
||||
- Generates both CJS and ESM entrypoints.
|
||||
|
||||
Tree-shaking is enabled (`treeshake.moduleSideEffects = false`). `sideEffects: false` in `package.json` allows downstream bundlers to drop unused exports.
|
||||
|
||||
## Design Notes / Non-goals
|
||||
- WS-Security: Implements only UsernameToken with password type Text or Digest (configurable via `wssePasswordType`). No nonce or timestamp included.
|
||||
- Idempotency: `bodId` generated per request unless provided; `ensureBodAndDates` centralizes creation.
|
||||
- Parsing: Focused on extracting status and op-specific identifiers; raw parsed STAR tree still accessible via `result.parsed`.
|
||||
- Status Handling: Treats `NO_MATCH` (codes like 2 or 213) as non-error with `success=true` so callers can differentiate empty results from failures.
|
||||
- Validation: Enforces only practical minimum & enumerations; does not attempt full schema compliance.
|
||||
- No automatic `fetch` transport fallback yet (axios chosen for reliability). You can replace `postSoap` if providing a compatible function returning raw response text and error semantics.
|
||||
- No built-in XSD validator; XSDs present for reference only.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
'use strict';
|
||||
// CJS access point for JSDoc typedefs
|
||||
module.exports = require('./types.js');
|
||||
@@ -47,9 +47,9 @@ function toCombinedSearchPayload(args = {}) {
|
||||
if (q.phone) kind = "phone";
|
||||
else if (q.license) kind = "license";
|
||||
else if (q.vin) kind = "vin";
|
||||
else if (q.nameRecId || q.custId) kind = "nameRecId";
|
||||
else if (q.nameRecId || q.custId) kind = "namerecid";
|
||||
else if (q.name && (q.name.fname || q.name.lname || q.name.mname || q.name.name)) kind = "name";
|
||||
else if (q.stkNo || q.stock) kind = "stkNo";
|
||||
else if (q.stkNo || q.stock) kind = "stkno";
|
||||
}
|
||||
|
||||
// Map loose aliases into the RR builder’s expected fields
|
||||
@@ -57,21 +57,41 @@ function toCombinedSearchPayload(args = {}) {
|
||||
|
||||
switch (kind) {
|
||||
case "phone":
|
||||
payload.kind = "phone";
|
||||
payload.phone = q.phone;
|
||||
break;
|
||||
case "license":
|
||||
payload.kind = "license";
|
||||
payload.license = q.license;
|
||||
break;
|
||||
case "vin":
|
||||
payload.kind = "vin";
|
||||
payload.vin = q.vin;
|
||||
break;
|
||||
case "namerecid":
|
||||
payload.kind = "nameRecId";
|
||||
payload.nameRecId = q.nameRecId || q.custId;
|
||||
break;
|
||||
case "name":
|
||||
payload.name = q.name; // { fname, lname, mname } or { name }
|
||||
case "name": {
|
||||
payload.kind = "name";
|
||||
const name = q.name;
|
||||
if (name.name) {
|
||||
payload.name = { name: name.name }; // For LName
|
||||
} else if (name.fname && name.lname) {
|
||||
payload.name = {
|
||||
fname: name.fname,
|
||||
lname: name.lname,
|
||||
...(name.mname ? { mname: name.mname } : {})
|
||||
}; // For FullName
|
||||
} else if (name.lname) {
|
||||
payload.name = { name: name.lname }; // Fallback to LName if only lname
|
||||
} else {
|
||||
// Invalid; but to handle gracefully, perhaps throw or skip
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "stkno":
|
||||
payload.kind = "stkNo";
|
||||
payload.stkNo = q.stkNo || q.stock;
|
||||
break;
|
||||
default:
|
||||
@@ -79,16 +99,20 @@ function toCombinedSearchPayload(args = {}) {
|
||||
payload.kind = q.kind; // may be undefined; RR lib will validate
|
||||
}
|
||||
|
||||
// Add compatible secondary fields for combinations
|
||||
if (q.vin && kind !== "vin") payload.vin = q.vin;
|
||||
if (q.phone && kind !== "phone") payload.phone = q.phone;
|
||||
if (q.license && kind !== "license") payload.license = q.license;
|
||||
|
||||
// Optional vehicle narrowing; the RR builder defaults to ANY/ANY/ANY if omitted
|
||||
if (q.make || q.model || q.year) {
|
||||
payload.make = q.make;
|
||||
payload.model = q.model;
|
||||
payload.year = q.year;
|
||||
payload.make = q.make || "ANY";
|
||||
payload.model = q.model || "ANY";
|
||||
payload.year = q.year || "ANY";
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined customer/service/vehicle search
|
||||
* @param bodyshop - bodyshop row (must include rr_dealerid & rr_configuration with store/branch)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// File: server/rr/rr-register-socket-events.js
|
||||
// PATCH: add helper + modify rr-export-job customer preselect logic
|
||||
|
||||
const RRLogger = require("../rr/rr-logger");
|
||||
const { rrCombinedSearch, rrGetAdvisors, rrGetParts } = require("../rr/rr-lookup");
|
||||
const { QueryJobData } = require("../rr/rr-job-helpers");
|
||||
@@ -125,6 +128,67 @@ function readAdvisorNo(payload, cached) {
|
||||
function registerRREvents({ socket, redisHelpers }) {
|
||||
const log = RRLogger(socket);
|
||||
|
||||
// --- NEW helper: run name + vin searches and merge ---
|
||||
async function rrMultiCustomerSearch(bodyshop, job) {
|
||||
const queries = [];
|
||||
|
||||
// Extract fields
|
||||
const fname = (job?.ownr_fn || job?.customer?.first_name || job?.customer?.firstName || "").trim();
|
||||
const lname = (job?.ownr_ln || job?.customer?.last_name || job?.customer?.lastName || "").trim();
|
||||
const vin = (job?.v_vin || job?.vehicle?.vin || job?.vin || "").trim();
|
||||
|
||||
// Build combined query if possible
|
||||
if (vin && (fname || lname)) {
|
||||
const nameObj = {};
|
||||
if (fname) nameObj.fname = fname;
|
||||
if (lname) nameObj.lname = lname;
|
||||
queries.push({
|
||||
kind: "name",
|
||||
name: nameObj,
|
||||
vin: vin,
|
||||
maxResults: 50
|
||||
});
|
||||
} else if (vin) {
|
||||
queries.push({ kind: "vin", vin: vin, maxResults: 50 });
|
||||
} else if (fname || lname) {
|
||||
// Standalone name: prefer FullName if both, else LName if only lname (though spec prefers combos)
|
||||
// Note: Standalone Last Name not in spec; use with caution or add MMY if available
|
||||
const nameObj = {};
|
||||
if (fname) nameObj.fname = fname;
|
||||
if (lname) nameObj.lname = lname;
|
||||
queries.push({
|
||||
kind: "name",
|
||||
name: nameObj,
|
||||
maxResults: 50
|
||||
});
|
||||
}
|
||||
|
||||
if (!queries.length) return [];
|
||||
|
||||
// Execute searches serially (could be Promise.all; keep serial for vendor rate safety)
|
||||
const all = [];
|
||||
for (const q of queries) {
|
||||
try {
|
||||
const res = await rrCombinedSearch(bodyshop, q);
|
||||
const norm = normalizeCustomerCandidates(res);
|
||||
all.push(...norm);
|
||||
} catch (e) {
|
||||
log("warn", "multi-search subquery failed", { kind: q.kind, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// De-dupe by custNo
|
||||
const seen = new Set();
|
||||
const merged = [];
|
||||
for (const c of all) {
|
||||
const k = c && c.custNo && String(c.custNo).trim();
|
||||
if (!k || seen.has(k)) continue;
|
||||
seen.add(k);
|
||||
merged.push({ custNo: String(c.custNo), name: c.name });
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// --------- Lookups (customer search → open table) ---------
|
||||
socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
|
||||
try {
|
||||
@@ -282,9 +346,8 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
// --------- Main export (Fortellis-style staging) ---------
|
||||
socket.on("rr-export-job", async (payload = {}) => {
|
||||
const _log = RRLogger(socket, { ns: "rr" });
|
||||
|
||||
try {
|
||||
// 1) Resolve job
|
||||
// (existing preamble unchanged)
|
||||
let job = payload.job || payload.txEnvelope?.job;
|
||||
const jobId = payload.jobId || payload.jobid || payload.txEnvelope?.jobId || job?.id;
|
||||
if (!job) {
|
||||
@@ -293,7 +356,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
}
|
||||
const ns = getTransactionType(job.id);
|
||||
|
||||
// Persist txEnvelope + job
|
||||
await redisHelpers.setSessionTransactionData(
|
||||
socket.id,
|
||||
ns,
|
||||
@@ -303,7 +365,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
);
|
||||
await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.JobData, job, defaultRRTTL);
|
||||
|
||||
// 2) Resolve bodyshop
|
||||
let bodyshopId = payload.bodyshopId || payload.bodyshopid || payload.bodyshopUUID || job?.bodyshop?.id;
|
||||
if (!bodyshopId) {
|
||||
const sess = await getSessionOrSocket(redisHelpers, socket);
|
||||
@@ -316,7 +377,6 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
? job.bodyshop
|
||||
: await getBodyshopForSocket({ bodyshopId, socket });
|
||||
|
||||
// 3) Resolve advisor number (from form or cache)
|
||||
const cachedAdvisor = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo);
|
||||
const advisorNo = readAdvisorNo(payload, cachedAdvisor);
|
||||
if (!advisorNo) {
|
||||
@@ -331,9 +391,7 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
defaultRRTTL
|
||||
);
|
||||
|
||||
// 4) Resolve selected customer (payload → cache)
|
||||
let selectedCust = null;
|
||||
|
||||
if (payload.selectedCustomer) {
|
||||
if (typeof payload.selectedCustomer === "object" && payload.selectedCustomer.custNo) {
|
||||
selectedCust = { custNo: String(payload.selectedCustomer.custNo) };
|
||||
@@ -350,27 +408,46 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedCust) {
|
||||
const cached = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer);
|
||||
if (cached) selectedCust = { custNo: String(cached) };
|
||||
}
|
||||
|
||||
// Flags
|
||||
const forceCreate = payload.forceCreate === true;
|
||||
const autoCreateOnNoMatch = payload.autoCreateOnNoMatch !== false; // default TRUE
|
||||
const autoCreateOnNoMatch = payload.autoCreateOnNoMatch !== false;
|
||||
|
||||
// 5) If no selection & not "forceCreate", try auto-search
|
||||
// --- MODIFIED: multi-search (name + vin) when beginning selection ---
|
||||
if (!selectedCust && !forceCreate) {
|
||||
const customerQuery = makeCustomerSearchPayloadFromJob(job);
|
||||
const vehicleQuery = makeVehicleSearchPayloadFromJob(job);
|
||||
const query = customerQuery || vehicleQuery;
|
||||
const mergedCandidates = await rrMultiCustomerSearch(bodyshop, job);
|
||||
|
||||
if (query) {
|
||||
_log("info", "rr-export-job:customer-preselect-search", { query, jobId });
|
||||
const searchRes = await rrCombinedSearch(bodyshop, query);
|
||||
if (mergedCandidates.length === 1) {
|
||||
selectedCust = { custNo: String(mergedCandidates[0].custNo) };
|
||||
await redisHelpers.setSessionTransactionData(
|
||||
socket.id,
|
||||
ns,
|
||||
RRCacheEnums.SelectedCustomer,
|
||||
selectedCust.custNo,
|
||||
defaultRRTTL
|
||||
);
|
||||
_log("info", "rr-export-job:auto-selected-customer(multi)", { jobId, custNo: selectedCust.custNo });
|
||||
} else if (mergedCandidates.length > 1) {
|
||||
socket.emit("rr-select-customer", mergedCandidates);
|
||||
socket.emit("rr-log-event", {
|
||||
level: "info",
|
||||
message: "RR: customer selection required (multi-search)",
|
||||
ts: Date.now()
|
||||
});
|
||||
return; // wait for user selection
|
||||
} else {
|
||||
// Fallback to original single-query logic (phone etc.) only if no multi results
|
||||
// (retain legacy behavior)
|
||||
const fallbackQuery = makeCustomerSearchPayloadFromJob(job) || makeVehicleSearchPayloadFromJob(job) || null;
|
||||
|
||||
if (fallbackQuery) {
|
||||
_log("info", "rr-export-job:fallback-single-query", { fallbackQuery, jobId });
|
||||
try {
|
||||
const searchRes = await rrCombinedSearch(bodyshop, fallbackQuery);
|
||||
const candidates = normalizeCustomerCandidates(searchRes);
|
||||
|
||||
if (candidates.length === 1) {
|
||||
selectedCust = { custNo: String(candidates[0].custNo) };
|
||||
await redisHelpers.setSessionTransactionData(
|
||||
@@ -380,17 +457,25 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
selectedCust.custNo,
|
||||
defaultRRTTL
|
||||
);
|
||||
_log("info", "rr-export-job:auto-selected-customer", { jobId, custNo: selectedCust.custNo });
|
||||
_log("info", "rr-export-job:auto-selected-customer(fallback)", {
|
||||
jobId,
|
||||
custNo: selectedCust.custNo
|
||||
});
|
||||
} else if (candidates.length > 1) {
|
||||
// multiple matches → table and stop
|
||||
socket.emit("rr-select-customer", candidates); // FE expects [{custNo,name}]
|
||||
socket.emit("rr-select-customer", candidates);
|
||||
socket.emit("rr-log-event", {
|
||||
level: "info",
|
||||
message: "RR: customer selection required",
|
||||
ts: Date.now()
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
}
|
||||
} catch (e) {
|
||||
_log("warn", "rr-export-job:fallback-query failed", { error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedCust) {
|
||||
if (autoCreateOnNoMatch) {
|
||||
const created = await createRRCustomer({ bodyshop, job, socket });
|
||||
const custNo = created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey;
|
||||
@@ -403,39 +488,26 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
selectedCust.custNo,
|
||||
defaultRRTTL
|
||||
);
|
||||
_log("info", "rr-export-job:auto-created-customer", { jobId, custNo: selectedCust.custNo });
|
||||
_log("info", "rr-export-job:auto-created-customer(multi-zero)", {
|
||||
jobId,
|
||||
custNo: selectedCust.custNo
|
||||
});
|
||||
} else {
|
||||
socket.emit("rr-customer-create-required");
|
||||
socket.emit("rr-log-event", { level: "info", message: "RR: create customer required", ts: Date.now() });
|
||||
socket.emit("rr-log-event", {
|
||||
level: "info",
|
||||
message: "RR: create customer required",
|
||||
ts: Date.now()
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no usable query → create or prompt
|
||||
if (autoCreateOnNoMatch) {
|
||||
const created = await createRRCustomer({ bodyshop, job, socket });
|
||||
const custNo = created?.custNo || created?.customerNo || created?.CustomerNo || created?.dmsRecKey;
|
||||
if (!custNo) throw new Error("RR create customer returned no custNo");
|
||||
selectedCust = { custNo: String(custNo) };
|
||||
await redisHelpers.setSessionTransactionData(
|
||||
socket.id,
|
||||
ns,
|
||||
RRCacheEnums.SelectedCustomer,
|
||||
selectedCust.custNo,
|
||||
defaultRRTTL
|
||||
);
|
||||
_log("info", "rr-export-job:auto-created-customer(no-query)", { jobId, custNo: selectedCust.custNo });
|
||||
} else {
|
||||
socket.emit("rr-customer-create-required");
|
||||
socket.emit("rr-log-event", { level: "info", message: "RR: create customer required", ts: Date.now() });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedCust?.custNo) throw new Error("RR export: selected customer missing custNo");
|
||||
|
||||
// 6) Perform export (ensure SV + create/update RO inside exportJobToRR)
|
||||
// (rest of existing export logic unchanged)
|
||||
const result = await exportJobToRR({
|
||||
bodyshop,
|
||||
job,
|
||||
@@ -469,9 +541,7 @@ function registerRREvents({ socket, redisHelpers }) {
|
||||
_log("error", `Error during RR export: ${error.message}`, { jobId, stack: error.stack });
|
||||
try {
|
||||
socket.emit("export-failed", { vendor: "rr", jobId, error: error.message });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user