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

This commit is contained in:
Dave
2025-11-12 16:13:23 -05:00
parent e3b4620d0c
commit 556cd993b9
6 changed files with 510 additions and 91 deletions

View File

@@ -3,7 +3,70 @@ const { RRClient } = require("./lib/index.cjs");
const { getRRConfigFromBodyshop } = require("./rr-config");
const RRLogger = require("./rr-logger");
// Build client + opts from bodyshop
const COUNTRY_MAP = {
US: "US",
USA: "US",
"UNITED STATES": "US",
CA: "CA",
CAN: "CA",
CANADA: "CA"
};
function toCountry2(v) {
const s = String(v || "")
.trim()
.toUpperCase();
if (!s) return "US"; // sane default
if (COUNTRY_MAP[s]) return COUNTRY_MAP[s];
// fallbacks: prefer 2-char; last resort: take first 2
return s.length === 2 ? s : s.slice(0, 2);
}
function normalizePhone(num) {
const d = String(num || "").replace(/\D/g, "");
const n = d.length === 11 && d.startsWith("1") ? d.slice(1) : d;
return n.slice(0, 10);
}
function normalizePostal(pc, country) {
const s = String(pc || "").trim();
if (country === "US") return s.replace(/[^0-9]/g, "").slice(0, 5);
if (country === "CA") return s.toUpperCase().replace(/\s+/g, "").slice(0, 6);
return s;
}
function sanitizeRRCustomerPayload(payload = {}) {
const out = { ...payload };
out.addresses = (payload.addresses || []).map((a) => {
const country = toCountry2(a.country);
return {
...a,
country,
state: String(a.state || "")
.toUpperCase()
.slice(0, 2),
postalCode: normalizePostal(a.postalCode, country)
};
});
out.phones = (payload.phones || []).map((p) => ({
...p,
number: normalizePhone(p.number)
}));
// trim names defensively (RR has various max lengths by site config)
if (out.firstName) out.firstName = String(out.firstName).trim().slice(0, 30);
if (out.lastName) out.lastName = String(out.lastName).trim().slice(0, 30);
return out;
}
/**
* Build an RR client + common opts from a bodyshop row
* @param bodyshop
* @returns {{client: *, opts: {routing: {dealerNumber: *, storeNumber: *, areaNumber: *}, envelope: {sender: {component: string, task: string, referenceId: string, creator: string, senderName: string}}}}}
*/
function buildClientAndOpts(bodyshop) {
const cfg = getRRConfigFromBodyshop(bodyshop);
const client = new RRClient({
@@ -29,19 +92,29 @@ function buildClientAndOpts(bodyshop) {
return { client, opts };
}
/**
* Strip all non-digit characters from a string
* @param s
* @returns {string}
*/
function digitsOnly(s) {
return String(s || "").replace(/\D/g, "");
}
/**
* Return a new array with only unique values from the input array
* @param arr
* @returns {any[]}
*/
function uniq(arr) {
return Array.from(new Set(arr));
}
/**
* Build a payload that matches the RR client expectations for insert/update:
* - ibFlag: 'I' (individual) or 'B' (business). If we have a first name, default to 'I', else 'B' if company present.
* - Must include lastName OR customerName.
* - addresses[] / phones[] / emails[] per the librarys toView() contract.
* Build RR customer payload from job.ownr_* fields, with optional overrides.
* @param job
* @param overrides
* @returns {{ibFlag: string, firstName, lastName, customerName, createdBy, customerType, addresses: [{type, line1: *, line2, city, state, postalCode, country}], phones: {number: *}[], emails: [{address: string}]}}
*/
function buildCustomerPayloadFromJob(job, overrides = {}) {
// Pull ONLY from job.ownr_* fields (no job.customer.*)
@@ -114,7 +187,8 @@ async function createRRCustomer({ bodyshop, job, overrides = {}, socket }) {
let res;
try {
res = await client.insertCustomer(payload, opts);
const safePayload = sanitizeRRCustomerPayload(payload);
res = await client.insertCustomer(safePayload, opts);
} catch (e) {
log("error", "RR insertCustomer transport error", { message: e?.message, stack: e?.stack, payload });
throw e;

View File

@@ -1,4 +1,3 @@
// server/rr/rr-job-export.js
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
const { buildClientAndOpts } = require("./rr-lookup");
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
@@ -8,6 +7,9 @@ const RRLogger = require("./rr-logger");
* Orchestrate an RR export (assumes custNo already resolved):
* - Ensure service vehicle (create flows)
* - Create or update the Repair Order
*
* NOTE: This function performs the create/update step and returns the RO data.
* "Mark exported" is handled later by the finalize step after cashiering.
*/
async function exportJobToRR(args) {
const { bodyshop, job, advisorNo, selectedCustomer, existing, socket } = args || {};
@@ -31,14 +33,15 @@ async function exportJobToRR(args) {
sender: {
...(opts?.envelope?.sender || {}),
task: "BSMRO",
referenceId: existing?.dmsRepairOrderId ? "Update" : "Insert"
// If we have an existing RO number we'll be updating, otherwise inserting
referenceId: existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId ? "Update" : "Insert"
}
}
};
// Ensure service vehicle for create flows (best-effort)
let svId = null;
if (!existing?.dmsRepairOrderId) {
if (!(existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId)) {
try {
const svRes = await ensureRRServiceVehicle({
bodyshop,
@@ -54,19 +57,33 @@ async function exportJobToRR(args) {
}
}
// Build RO payload (now includes DeptType/departmentType + variants)
// Build RO payload for create/update
const payload = buildRRRepairOrderPayload({
job,
selectedCustomer: { customerNo: String(selected), custNo: String(selected) },
advisorNo: String(advisorNo)
});
const rrRes = existing?.dmsRepairOrderId
? await client.updateRepairOrder({ ...payload, dmsRepairOrderId: existing.dmsRepairOrderId }, finalOpts)
// Canonical update key is "roNo" (prefer DMS RO number); accept fallbacks from "existing"
const roNoForUpdate = existing?.roNo || existing?.dmsRoNo || existing?.dmsRepairOrderId || null;
const rrRes = roNoForUpdate
? await client.updateRepairOrder({ ...payload, roNo: String(roNoForUpdate) }, finalOpts) // ✅ use roNo on update
: await client.createRepairOrder(payload, finalOpts);
const data = rrRes?.data || null;
const roStatus = data?.roStatus || null;
// Extract canonical roNo you'll need for finalize step
const roNo =
data?.dmsRoNo ??
data?.outsdRoNo ??
roStatus?.dmsRoNo ??
roStatus?.DMSRoNo ??
roStatus?.outsdRoNo ??
roStatus?.OutsdRoNo ??
null;
return {
success: rrRes?.success === true || roStatus?.status === "Success",
data,
@@ -75,8 +92,85 @@ async function exportJobToRR(args) {
xml: rrRes?.xml,
parsed: rrRes?.parsed,
customerNo: String(selected),
svId
svId,
roNo
};
}
module.exports = { exportJobToRR };
/**
* Finalize an RR Repair Order by sending finalUpdate: "Y".
* The caller should pass the canonical `roNo` if available (prefer DMS RO #).
* If not provided, we *safely* fall back to the external (Outsd) RO number.
*/
async function finalizeRRRepairOrder(args) {
const { bodyshop, job, advisorNo, customerNo, roNo, vin, socket } = args || {};
const log = RRLogger(socket, { ns: "rr-finalize" });
if (!bodyshop) throw new Error("finalizeRRRepairOrder: bodyshop is required");
if (!job) throw new Error("finalizeRRRepairOrder: job is required");
if (!advisorNo) throw new Error("finalizeRRRepairOrder: advisorNo is required");
if (!customerNo) throw new Error("finalizeRRRepairOrder: customerNo is required");
// The external (Outsd) RO is our deterministic fallback and correlation id.
const externalRo = job?.ro_number ?? job?.id;
if (externalRo == null) throw new Error("finalizeRRRepairOrder: outsdRoNo (job.ro_number/id) is required");
// Prefer DMS RO for update; fall back to external when DMS RO isn't known
const roNoToSend = roNo ? String(roNo) : String(externalRo);
const { client, opts } = buildClientAndOpts(bodyshop);
const finalOpts = {
...opts,
envelope: {
...(opts?.envelope || {}),
sender: {
...(opts?.envelope?.sender || {}),
task: "BSMRO",
referenceId: "Update"
}
}
};
const cleanVin =
(job?.v_vin || vin || "")
.toString()
.replace(/[^A-Za-z0-9]/g, "")
.toUpperCase()
.slice(0, 17) || undefined;
// IMPORTANT: include "roNo" on updates (RR requires it). Also send outsdRoNo for correlation.
const payload = {
roNo: roNoToSend, // ✅ REQUIRED BY RR on update
outsdRoNo: String(externalRo),
finalUpdate: "Y",
departmentType: "B",
customerNo: String(customerNo),
advisorNo: String(advisorNo),
vin: cleanVin,
mileageIn: job?.kmin,
mileageOut: job?.kmout,
estimate: { estimateType: "Final" }
};
log("info", "RR finalize updateRepairOrder", {
roNo: roNoToSend,
outsdRoNo: String(externalRo),
customerNo: String(customerNo),
advisorNo: String(advisorNo)
});
const rrRes = await client.updateRepairOrder(payload, finalOpts);
const data = rrRes?.data || null;
const roStatus = data?.roStatus || null;
return {
success: rrRes?.success === true || roStatus?.status === "Success",
data,
roStatus,
statusBlocks: rrRes?.statusBlocks || [],
xml: rrRes?.xml,
parsed: rrRes?.parsed
};
}
module.exports = { exportJobToRR, finalizeRRRepairOrder };

View File

@@ -1,7 +1,7 @@
const CreateRRLogEvent = require("./rr-logger-event");
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup");
const { QueryJobData } = require("./rr-job-helpers");
const { exportJobToRR } = require("./rr-job-export");
const { exportJobToRR, finalizeRRRepairOrder } = require("./rr-job-export");
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
const { createRRCustomer } = require("./rr-customers");
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
@@ -317,7 +317,7 @@ function registerRREvents({ socket, redisHelpers }) {
}
});
// ================= Fortellis-style two-step export =================
// ================= Fortellis-style two-step export (RR only) =================
// 1) Stage export -> search (Full Name + VIN) -> emit rr-select-customer
socket.on("rr-export-job", async ({ jobid, jobId, txEnvelope } = {}) => {
const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null);
@@ -385,7 +385,7 @@ function registerRREvents({ socket, redisHelpers }) {
}
});
// 2) Selection (or create) -> ensure vehicle -> export
// 2) Selection (or create) -> ensure vehicle -> CREATE RO (do not mark exported)
socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
let bodyshop = null;
@@ -515,7 +515,6 @@ function registerRREvents({ socket, redisHelpers }) {
client,
routing,
bodyshop,
// Normalize for any internal checks:
selectedCustomerNo: effectiveCustNo,
custNo: effectiveCustNo,
customerNo: effectiveCustNo,
@@ -532,7 +531,6 @@ function registerRREvents({ socket, redisHelpers }) {
const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor);
if (!advisorNo) {
CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo)`);
// Failure log (no advisor)
await insertRRFailedExportLog({
socket,
jobId: rid,
@@ -552,8 +550,8 @@ function registerRREvents({ socket, redisHelpers }) {
defaultRRTTL
);
// Export
CreateRRLogEvent(socket, "DEBUG", `{4} Performing RR export`);
// CREATE/UPDATE (first step only)
CreateRRLogEvent(socket, "DEBUG", `{4} Performing RR create/update (step 1)`);
const result = await exportJobToRR({
bodyshop,
job,
@@ -563,22 +561,60 @@ function registerRREvents({ socket, redisHelpers }) {
socket
});
if (result?.success) {
CreateRRLogEvent(socket, "DEBUG", `{5} Export success`, { roStatus: result.roStatus });
// Cache raw export result + pending RO number for finalize
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.ExportResult,
result || {},
defaultRRTTL
);
// ✅ Mark exported + success log (with metadata)
await markRRExportSuccess({
socket,
jobId: rid,
job,
bodyshop,
result
if (result?.success) {
const data = result?.data || {};
// Prefer explicit return from export function; then fall back to fields
const dmsRoNo =
result?.roNo ?? data?.dmsRoNo ?? data?.DMSRoNo ?? data?.roStatus?.dmsRoNo ?? data?.roStatus?.DMSRoNo ?? null;
const outsdRoNo =
data?.outsdRoNo ??
data?.OutsdRoNo ??
data?.roStatus?.outsdRoNo ??
data?.roStatus?.OutsdRoNo ??
job?.ro_number ??
job?.id ??
null;
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.PendingRO,
{
outsdRoNo,
dmsRoNo,
customerNo: String(effectiveCustNo),
advisorNo: String(advisorNo),
vin: job?.v_vin || null
},
defaultRRTTL
);
CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for cashiering.`, {
dmsRoNo: dmsRoNo || null,
outsdRoNo: outsdRoNo || null
});
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: result.roStatus });
ack?.({ ok: true, result });
// Tell FE to prompt for "Finished/Close"
socket.emit("rr-cashiering-required", { jobId: rid, dmsRoNo, outsdRoNo });
// Still emit info result if you want
socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result });
// ACK but indicate it's pending finalize
ack?.({ ok: true, pendingFinalize: true, dmsRoNo, outsdRoNo, result });
} else {
// NEW: classify vendor status for a friendly FE message
// classify & fail (no finalize)
const vendorStatusCode = Number(
result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? result?.statusBlocks?.transaction?.statusCode
);
@@ -587,12 +623,11 @@ function registerRREvents({ socket, redisHelpers }) {
message: result?.roStatus?.message ?? result?.roStatus?.Message ?? result?.error ?? "RR export failed"
});
CreateRRLogEvent(socket, "ERROR", `Export failed`, {
CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
roStatus: result?.roStatus,
classification: cls
});
// ❌ Failure log (with classification + bits of response)
await insertRRFailedExportLog({
socket,
jobId: rid,
@@ -609,8 +644,6 @@ function registerRREvents({ socket, redisHelpers }) {
error: cls?.friendlyMessage || result?.error || "RR export failed",
...cls
});
// Optional: a user-focused channel if you want to show inline banners
socket.emit("rr-user-notice", { jobId: rid, ...cls });
ack?.({
ok: false,
@@ -619,15 +652,6 @@ function registerRREvents({ socket, redisHelpers }) {
classification: cls
});
}
await redisHelpers.setSessionTransactionData(
socket.id,
ns,
RRCacheEnums.ExportResult,
result || {},
defaultRRTTL
);
socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result });
} catch (error) {
const cls = classifyRRVendorError(error);
@@ -640,9 +664,7 @@ function registerRREvents({ socket, redisHelpers }) {
jobid: rid
});
// ❌ Failure log for thrown error path
try {
// Load bodyshop/job if not loaded yet (best-effort)
if (!bodyshop || !job) {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
@@ -651,7 +673,7 @@ function registerRREvents({ socket, redisHelpers }) {
(await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
}
} catch {
// ignore
//
}
await insertRRFailedExportLog({
@@ -670,10 +692,155 @@ function registerRREvents({ socket, redisHelpers }) {
error: error.message,
...cls
});
// Optional UX hook for inline banners/toasts
socket.emit("rr-user-notice", { jobId: rid, ...cls });
} catch {
/* ignore */
//
}
ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
}
});
// 3) Finalize -> updateRepairOrder(finalUpdate: "Y") -> mark exported
socket.on("rr-finalize-repair-order", async ({ jobid, jobId } = {}, ack) => {
const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
let bodyshop = null;
let job = null;
try {
if (!rid) throw new Error("jobid required for finalize");
const ns = getTransactionType(rid);
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData);
if (!job) job = await QueryJobData({ redisHelpers }, rid);
const pending = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO);
const advisorNo = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo);
const selectedCustomerNo = await redisHelpers.getSessionTransactionData(
socket.id,
ns,
RRCacheEnums.SelectedCustomer
);
if (!advisorNo) throw new Error("Advisor missing in session");
if (!selectedCustomerNo) throw new Error("Customer number missing in session");
// Prefer cached outsdRoNo; fall back to our deterministic external number
const outsdRoNo = pending?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
// Prefer DMS RO for update, but finalize() will safely fall back to Outsd if missing
const dmsRoNo = pending?.dmsRoNo ?? pending?.roNo ?? null;
CreateRRLogEvent(socket, "DEBUG", `{6} Finalizing RR RO`, {
jobId: rid,
outsdRoNo,
dmsRoNo,
advisorNo,
customerNo: selectedCustomerNo
});
const finalizeResult = await finalizeRRRepairOrder({
bodyshop,
job,
advisorNo: String(advisorNo),
customerNo: String(selectedCustomerNo),
roNo: dmsRoNo, // ✅ RR requires roNo; finalize() will fall back to outsdRoNo if this is absent
vin: pending?.vin,
socket
});
if (finalizeResult?.success) {
CreateRRLogEvent(socket, "INFO", `{7} Finalize success; marking exported`, { dmsRoNo, outsdRoNo });
// ✅ Mark exported + success log
await markRRExportSuccess({
socket,
jobId: rid,
job,
bodyshop,
result: finalizeResult
});
// Clean pending key
try {
await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO, null, 1);
} catch {
//
}
socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: finalizeResult?.roStatus });
ack?.({ ok: true, result: finalizeResult });
} else {
const vendorStatusCode = Number(
finalizeResult?.roStatus?.statusCode ??
finalizeResult?.roStatus?.StatusCode ??
finalizeResult?.statusBlocks?.transaction?.statusCode
);
const cls = classifyRRVendorError({
code: vendorStatusCode,
message:
finalizeResult?.roStatus?.message ??
finalizeResult?.roStatus?.Message ??
finalizeResult?.error ??
"RR finalize failed"
});
await insertRRFailedExportLog({
socket,
jobId: rid,
job,
bodyshop,
error: new Error(cls.friendlyMessage || finalizeResult?.error || "RR finalize failed"),
classification: cls,
result: finalizeResult
});
socket.emit("export-failed", {
vendor: "rr",
jobId: rid,
error: cls?.friendlyMessage || finalizeResult?.error || "RR finalize failed",
...cls
});
ack?.({ ok: false, error: cls.friendlyMessage || "RR finalize failed", classification: cls });
}
} catch (error) {
const cls = classifyRRVendorError(error);
CreateRRLogEvent(socket, "ERROR", `Error during RR finalize`, {
error: error.message,
vendorStatusCode: cls.vendorStatusCode,
code: cls.errorCode,
friendly: cls.friendlyMessage,
stack: error.stack,
jobid: rid
});
try {
if (!bodyshop || !job) {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
job =
job ||
(await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
}
} catch {
//
}
await insertRRFailedExportLog({
socket,
jobId: rid,
job,
bodyshop,
error,
classification: cls
});
try {
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message, ...cls });
} catch {
//
}
ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });

View File

@@ -70,31 +70,6 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => {
const country = chosen?.Country ?? chosen?.CountryCode ?? chosen?.country ?? undefined;
const county = chosen?.County ?? chosen?.county ?? undefined; // << added
// instrumentation (kept minimal; County is now expected)
if ((process.env.RR_DEBUG_ADDR ?? "1") !== "0") {
const allowed = new Set([
"Type",
"Addr1",
"AddressLine1",
"Line1",
"Street1",
"Addr2",
"AddressLine2",
"Line2",
"Street2",
"City",
"State",
"StateOrProvince",
"Zip",
"PostalCode",
"Country",
"CountryCode",
"County"
]);
const unknown = Object.keys(chosen || {}).filter((k) => !allowed.has(k));
if (unknown.length) console.log("[RR:normCandidates] Unexpected address keys seen:", unknown);
}
if (!line1 && !city && !state && !postalCode && !country && !county) return null;
return { line1, line2, city, state, postalCode, country, county };
};
@@ -113,7 +88,7 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => {
const address = pickAddr(nci?.Address);
// NEW: fallback to NameRecId when no ServVehicle/CustomerNo exists (e.g., pure name search)
// fallback to NameRecId when no ServVehicle/CustomerNo exists (e.g., pure name search)
const nameRecIdRaw = nci?.NameId?.NameRecId;
const nameRecId = nameRecIdRaw != null ? String(nameRecIdRaw).trim() : "";
@@ -136,14 +111,13 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => {
out.push(item);
}
} else if (nameRecId) {
// Use NameRecId as the identifier; this is what the RR "name" search provides
// Use NameRecId as the identifier
const cno = nameRecId;
const item = {
custNo: cno,
name: name || `Customer ${cno}`,
address: address || undefined
};
// owner flag cannot be inferred without a VIN owner set
out.push(item);
}
}
@@ -186,7 +160,7 @@ const readAdvisorNo = (payload, cached) => {
/**
* Cache enum keys for RR session transaction data
* @type {{txEnvelope: string, JobData: string, SelectedCustomer: string, AdvisorNo: string, VINCandidates: string, SelectedVin: string, ExportResult: string}}
* @type {{txEnvelope: string, JobData: string, SelectedCustomer: string, AdvisorNo: string, VINCandidates: string, SelectedVin: string, ExportResult: string, PendingRO: string}}
*/
const RRCacheEnums = {
txEnvelope: "RR.txEnvelope",
@@ -195,7 +169,8 @@ const RRCacheEnums = {
AdvisorNo: "RR.AdvisorNo",
VINCandidates: "RR.VINCandidates",
SelectedVin: "RR.SelectedVin",
ExportResult: "RR.ExportResult"
ExportResult: "RR.ExportResult",
PendingRO: "RR.PendingRO" // NEW: cache created RO to finalize later
};
/**