feature/IO-3255-simplified-parts-management - Refactor / Working Change Request

This commit is contained in:
Dave Richer
2025-07-07 13:09:55 -04:00
parent 8bc6bea4b2
commit bd2720f534
5 changed files with 129 additions and 156 deletions

View File

@@ -1,12 +1,15 @@
const admin = require("firebase-admin"); const admin = require("firebase-admin");
const client = require("../../graphql-client/graphql-client").client; const client = require("../../../graphql-client/graphql-client").client;
const DefaultNewShop = require("./defaultNewShop.json"); const DefaultNewShop = require("../defaultNewShop.json");
const {
CHECK_EXTERNAL_SHOP_ID,
CREATE_SHOP,
DELETE_VENDORS_BY_SHOP,
DELETE_SHOP,
CREATE_USER
} = require("../partsManagement.queries");
/**
* Ensures that the required fields are present in the payload.
* @param payload
* @param fields
*/
const requireFields = (payload, fields) => { const requireFields = (payload, fields) => {
for (const field of fields) { for (const field of fields) {
if (!payload[field]) { if (!payload[field]) {
@@ -15,11 +18,6 @@ const requireFields = (payload, fields) => {
} }
}; };
/**
* Ensures that the email is not already registered in Firebase.
* @param email
* @returns {Promise<void>}
*/
const ensureEmailNotRegistered = async (email) => { const ensureEmailNotRegistered = async (email) => {
try { try {
await admin.auth().getUserByEmail(email); await admin.auth().getUserByEmail(email);
@@ -31,113 +29,41 @@ const ensureEmailNotRegistered = async (email) => {
} }
}; };
/**
* Creates a new Firebase user with the provided email and optional password.
* @param email
* @param password
* @returns {Promise<UserRecord>}
*/
const createFirebaseUser = async (email, password = null) => { const createFirebaseUser = async (email, password = null) => {
const userData = { email }; const userData = { email };
if (password) { if (password) userData.password = password;
userData.password = password;
}
return admin.auth().createUser(userData); return admin.auth().createUser(userData);
}; };
/**
* Deletes a Firebase user by their UID.
* @param uid
* @returns {Promise<void>}
*/
const deleteFirebaseUser = async (uid) => { const deleteFirebaseUser = async (uid) => {
return admin.auth().deleteUser(uid); return admin.auth().deleteUser(uid);
}; };
/**
* Generates a password reset link for the given email.
* @param email
* @returns {Promise<string>}
*/
const generateResetLink = async (email) => { const generateResetLink = async (email) => {
return admin.auth().generatePasswordResetLink(email); return admin.auth().generatePasswordResetLink(email);
}; };
/**
* Ensures that the external shop ID is unique in the database.
* @param externalId
* @returns {Promise<void>}
*/
const ensureExternalIdUnique = async (externalId) => { const ensureExternalIdUnique = async (externalId) => {
const query = ` const resp = await client.request(CHECK_EXTERNAL_SHOP_ID, { key: externalId });
query CHECK_KEY($key: String!) {
bodyshops(where: { external_shop_id: { _eq: $key } }) {
external_shop_id
}
}`;
const resp = await client.request(query, { key: externalId });
if (resp.bodyshops.length) { if (resp.bodyshops.length) {
throw { status: 400, message: `external_shop_id '${externalId}' is already in use.` }; throw { status: 400, message: `external_shop_id '${externalId}' is already in use.` };
} }
}; };
/**
* Inserts a new bodyshop into the database.
* @param input
* @returns {Promise<*>}
*/
const insertBodyshop = async (input) => { const insertBodyshop = async (input) => {
const mutation = ` const resp = await client.request(CREATE_SHOP, { bs: input });
mutation CREATE_SHOP($bs: bodyshops_insert_input!) {
insert_bodyshops_one(object: $bs) { id }
}`;
const resp = await client.request(mutation, { bs: input });
return resp.insert_bodyshops_one.id; return resp.insert_bodyshops_one.id;
}; };
/**
* Deletes all vendors associated with a specific shop ID.
* @param shopId
* @returns {Promise<void>}
*/
const deleteVendorsByShop = async (shopId) => { const deleteVendorsByShop = async (shopId) => {
const mutation = ` await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
mutation DELETE_VENDORS($shopId: uuid!) {
delete_vendors(where: { shopid: { _eq: $shopId } }) {
affected_rows
}
}`;
await client.request(mutation, { shopId });
}; };
/**
* Deletes a bodyshop by its ID.
* @param shopId
* @returns {Promise<void>}
*/
const deleteBodyshop = async (shopId) => { const deleteBodyshop = async (shopId) => {
const mutation = ` await client.request(DELETE_SHOP, { id: shopId });
mutation DELETE_SHOP($id: uuid!) {
delete_bodyshops_by_pk(id: $id) { id }
}`;
await client.request(mutation, { id: shopId });
}; };
/**
* Inserts a new user association into the database.
* @param uid
* @param email
* @param shopId
* @returns {Promise<*>}
*/
const insertUserAssociation = async (uid, email, shopId) => { const insertUserAssociation = async (uid, email, shopId) => {
const mutation = `
mutation CREATE_USER($u: users_insert_input!) {
insert_users_one(object: $u) {
id: authid
email
}
}`;
const vars = { const vars = {
u: { u: {
email, email,
@@ -148,22 +74,15 @@ const insertUserAssociation = async (uid, email, shopId) => {
} }
} }
}; };
const resp = await client.request(mutation, vars); const resp = await client.request(CREATE_USER, vars);
return resp.insert_users_one; return resp.insert_users_one;
}; };
/**
* Handles the provisioning of a new parts management shop and user.
* @param req
* @param res
* @returns {Promise<*>}
*/
const partsManagementProvisioning = async (req, res) => { const partsManagementProvisioning = async (req, res) => {
const { logger } = req; const { logger } = req;
const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
try { try {
// Validate inputs
await ensureEmailNotRegistered(p.userEmail); await ensureEmailNotRegistered(p.userEmail);
requireFields(p, [ requireFields(p, [
"external_shop_id", "external_shop_id",
@@ -184,7 +103,6 @@ const partsManagementProvisioning = async (req, res) => {
ioadmin: true ioadmin: true
}); });
// Create shop
const shopInput = { const shopInput = {
shopname: p.shopname, shopname: p.shopname,
address1: p.address1, address1: p.address1,
@@ -204,7 +122,7 @@ const partsManagementProvisioning = async (req, res) => {
headerMargin: DefaultNewShop.logo_img_path.headerMargin headerMargin: DefaultNewShop.logo_img_path.headerMargin
}, },
features: { features: {
partsManagementOnly: true // This is a parts management only shop partsManagementOnly: true
}, },
md_ro_statuses: DefaultNewShop.md_ro_statuses, md_ro_statuses: DefaultNewShop.md_ro_statuses,
vendors: { vendors: {
@@ -227,14 +145,12 @@ const partsManagementProvisioning = async (req, res) => {
})) }))
} }
}; };
const newShopId = await insertBodyshop(shopInput);
// Create user + association const newShopId = await insertBodyshop(shopInput);
const userRecord = await createFirebaseUser(p.userEmail, p.userPassword); const userRecord = await createFirebaseUser(p.userEmail, p.userPassword);
let resetLink = null; let resetLink = null;
if (!p.userPassword) { if (!p.userPassword) resetLink = await generateResetLink(p.userEmail);
resetLink = await generateResetLink(p.userEmail);
}
const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId); const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId);
return res.status(200).json({ return res.status(200).json({
@@ -242,7 +158,7 @@ const partsManagementProvisioning = async (req, res) => {
user: { user: {
id: createdUser.id, id: createdUser.id,
email: createdUser.email, email: createdUser.email,
resetLink: resetLink || undefined // Only include resetLink if it exists resetLink: resetLink || undefined
} }
}); });
} catch (err) { } catch (err) {
@@ -251,18 +167,17 @@ const partsManagementProvisioning = async (req, res) => {
detail: err.detail || err detail: err.detail || err
}); });
// Cleanup on failure
if (err.userRecord) { if (err.userRecord) {
await deleteFirebaseUser(err.userRecord.uid).catch(() => { await deleteFirebaseUser(err.userRecord.uid).catch(() => {
// Ignore errors during user deletion cleanup /* empty */
}); });
} }
if (err.newShopId) { if (err.newShopId) {
await deleteVendorsByShop(err.newShopId).catch(() => { await deleteVendorsByShop(err.newShopId).catch(() => {
// Ignore errors during vendor deletion cleanup /* empty */
}); });
await deleteBodyshop(err.newShopId).catch(() => { await deleteBodyshop(err.newShopId).catch(() => {
// Ignore errors during shop deletion cleanup /* empty */
}); });
} }

View File

@@ -1,8 +1,8 @@
// no-dd-sa:javascript-code-style/assignment-name // no-dd-sa:javascript-code-style/assignment-name
// CamelCase is used for GraphQL and database fields. // CamelCase is used for GraphQL and database fields.
const client = require("../../graphql-client/graphql-client").client; const client = require("../../../graphql-client/graphql-client").client;
const { parseXml, normalizeXmlObject } = require("./partsManagementUtils"); const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
// GraphQL Queries and Mutations // GraphQL Queries and Mutations
const { const {
@@ -10,7 +10,7 @@ const {
GET_VEHICLE_BY_SHOP_VIN, GET_VEHICLE_BY_SHOP_VIN,
INSERT_OWNER, INSERT_OWNER,
INSERT_JOB_WITH_LINES INSERT_JOB_WITH_LINES
} = require("./partsManagement.queries"); } = require("../partsManagement.queries");
// Defaults // Defaults
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN";

View File

@@ -1,19 +1,19 @@
// no-dd-sa:javascript-code-style/assignment-name // no-dd-sa:javascript-code-style/assignment-name
// Handler for VehicleDamageEstimateChgRq // Handler for VehicleDamageEstimateChgRq
const client = require("../../graphql-client/graphql-client").client; const client = require("../../../graphql-client/graphql-client").client;
const { parseXml, normalizeXmlObject } = require("./partsManagementUtils"); const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
const { const {
GET_JOB_BY_CLAIM_OR_DOCID, GET_JOB_BY_CLAIM,
UPDATE_JOB_BY_PK, UPDATE_JOB_BY_ID,
UPSERT_JOBLINES, UPSERT_JOBLINES,
DELETE_JOBLINES_BY_IDS DELETE_JOBLINES_BY_IDS
} = require("./partsManagement.queries"); } = require("../partsManagement.queries");
const findJob = async (claimNum, documentId, logger) => { const findJob = async (shopId, claimNum, logger) => {
try { try {
const { jobs } = await client.request(GET_JOB_BY_CLAIM_OR_DOCID, { claimNum, documentId }); const { jobs } = await client.request(GET_JOB_BY_CLAIM, { shopid: shopId, clm_no: claimNum });
return jobs?.[0] || null; return jobs?.[0] || null;
} catch (err) { } catch (err) {
logger.log("parts-job-lookup-failed", "error", null, null, { error: err }); logger.log("parts-job-lookup-failed", "error", null, null, { error: err });
@@ -33,10 +33,11 @@ const extractUpdatedJobData = (rq) => {
}; };
}; };
const extractUpdatedJobLines = (addsChgs = {}) => { const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
const lines = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || []]; const lines = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || []];
return lines.map((line) => ({ return lines.map((line) => ({
jobid: jobId,
line_no: parseInt(line.LineNum, 10), line_no: parseInt(line.LineNum, 10),
unq_seq: parseInt(line.UniqueSequenceNum, 10), unq_seq: parseInt(line.UniqueSequenceNum, 10),
status: line.LineStatusCode || null, status: line.LineStatusCode || null,
@@ -51,7 +52,7 @@ const extractUpdatedJobLines = (addsChgs = {}) => {
lbr_op: line.LaborInfo?.LaborOperation || null, lbr_op: line.LaborInfo?.LaborOperation || null,
lbr_amt: parseFloat(line.LaborInfo?.LaborAmt || 0), lbr_amt: parseFloat(line.LaborInfo?.LaborAmt || 0),
notes: line.LineMemo || null, notes: line.LineMemo || null,
manual_line: line.ManualLineInd || null manual_line: line.ManualLineInd === "1"
})); }));
}; };
@@ -71,22 +72,20 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
const shopId = rq.ShopID; const shopId = rq.ShopID;
const claimNum = rq.ClaimInfo?.ClaimNum; const claimNum = rq.ClaimInfo?.ClaimNum;
const documentId = rq.DocumentInfo?.DocumentID;
if (!shopId || !claimNum) return res.status(400).send("Missing ShopID or ClaimNum"); if (!shopId || !claimNum) return res.status(400).send("Missing ShopID or ClaimNum");
const job = await findJob(claimNum, documentId, logger); const job = await findJob(shopId, claimNum, logger);
if (!job) return res.status(404).send("Job not found"); if (!job) return res.status(404).send("Job not found");
const updatedJobData = extractUpdatedJobData(rq); const updatedJobData = extractUpdatedJobData(rq);
const updatedLines = extractUpdatedJobLines(rq.AddsChgs); const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id);
const deletedLineIds = extractDeletions(rq.Deletions); const deletedLineIds = extractDeletions(rq.Deletions);
await client.request(UPDATE_JOB_BY_PK, { id: job.id, changes: updatedJobData }); await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
if (updatedLines.length > 0) { if (updatedLines.length > 0) {
await client.request(UPSERT_JOBLINES, { await client.request(UPSERT_JOBLINES, {
jobid: job.id,
joblines: updatedLines joblines: updatedLines
}); });
} }

View File

@@ -53,34 +53,50 @@ const UPDATE_JOB_BY_ID = `
`; `;
const UPSERT_JOBLINES = ` const UPSERT_JOBLINES = `
mutation UpsertJoblines($joblines: [joblines_insert_input!]!) { mutation UpsertJoblines($joblines: [joblines_insert_input!]!) {
insert_joblines( insert_joblines(
objects: $joblines objects: $joblines
on_conflict: { on_conflict: {
constraint: joblines_jobid_line_no_unq_seq_key constraint: joblines_pkey
update_columns: [ update_columns: [
status jobid
line_desc status
part_type line_desc
part_qty part_type
oem_partno part_qty
db_price oem_partno
act_price db_price
mod_lbr_ty act_price
mod_lb_hrs mod_lbr_ty
lbr_op mod_lb_hrs
lbr_amt lbr_op
notes lbr_amt
] notes
} manual_line
) { ]
}
) {
affected_rows
}
}
`;
const DELETE_JOBLINES_BY_JOBID = `
mutation DeleteJoblinesByJobId($jobid: uuid!) {
delete_joblines(where: { jobid: { _eq: $jobid } }) {
affected_rows affected_rows
} }
} }
`; `;
const DELETE_JOBLINES_BY_JOBID = `
mutation DeleteJoblinesByJobId($jobid: uuid!) { const DELETE_JOBLINES_BY_IDS = `
delete_joblines(where: { jobid: { _eq: $jobid } }) { mutation DeleteJoblinesByIds($jobid: uuid!, $unqSeqs: [Int!]!) {
delete_joblines(
where: {
jobid: { _eq: $jobid },
unq_seq: { _in: $unqSeqs }
}
) {
affected_rows affected_rows
} }
} }
@@ -94,6 +110,43 @@ const INSERT_JOBLINES = `
} }
`; `;
const CHECK_EXTERNAL_SHOP_ID = `
query CHECK_KEY($key: String!) {
bodyshops(where: { external_shop_id: { _eq: $key } }) {
external_shop_id
}
}
`;
const CREATE_SHOP = `
mutation CREATE_SHOP($bs: bodyshops_insert_input!) {
insert_bodyshops_one(object: $bs) { id }
}
`;
const DELETE_VENDORS_BY_SHOP = `
mutation DELETE_VENDORS($shopId: uuid!) {
delete_vendors(where: { shopid: { _eq: $shopId } }) {
affected_rows
}
}
`;
const DELETE_SHOP = `
mutation DELETE_SHOP($id: uuid!) {
delete_bodyshops_by_pk(id: $id) { id }
}
`;
const CREATE_USER = `
mutation CREATE_USER($u: users_insert_input!) {
insert_users_one(object: $u) {
id: authid
email
}
}
`;
module.exports = { module.exports = {
GET_BODYSHOP_STATUS, GET_BODYSHOP_STATUS,
GET_VEHICLE_BY_SHOP_VIN, GET_VEHICLE_BY_SHOP_VIN,
@@ -101,7 +154,13 @@ module.exports = {
INSERT_JOB_WITH_LINES, INSERT_JOB_WITH_LINES,
GET_JOB_BY_CLAIM, GET_JOB_BY_CLAIM,
UPDATE_JOB_BY_ID, UPDATE_JOB_BY_ID,
DELETE_JOBLINES_BY_JOBID,
UPSERT_JOBLINES, UPSERT_JOBLINES,
INSERT_JOBLINES DELETE_JOBLINES_BY_JOBID,
DELETE_JOBLINES_BY_IDS,
INSERT_JOBLINES,
CHECK_EXTERNAL_SHOP_ID,
CREATE_SHOP,
DELETE_VENDORS_BY_SHOP,
DELETE_SHOP,
CREATE_USER
}; };

View File

@@ -19,10 +19,10 @@ if (typeof VSSTA_INTEGRATION_SECRET === "string" && VSSTA_INTEGRATION_SECRET.len
if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_INTEGRATION_SECRET.length > 0) { if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_INTEGRATION_SECRET.length > 0) {
const XML_BODY_LIMIT = "10mb"; // Set a limit for XML body size const XML_BODY_LIMIT = "10mb"; // Set a limit for XML body size
const partsManagementProvisioning = require("../integrations/partsManagement/partsManagementProvisioning"); const partsManagementProvisioning = require("../integrations/partsManagement/endpoints/partsManagementProvisioning");
const partsManagementIntegrationMiddleware = require("../middleware/partsManagementIntegrationMiddleware"); const partsManagementIntegrationMiddleware = require("../middleware/partsManagementIntegrationMiddleware");
const partsManagementVehicleDamageEstimateAddRq = require("../integrations/partsManagement/vehicleDamageEstimateAddRq"); const partsManagementVehicleDamageEstimateAddRq = require("../integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq");
const partsManagementVehicleDamageEstimateChqRq = require("../integrations/partsManagement/vehicleDamageEstimateChgRq"); const partsManagementVehicleDamageEstimateChqRq = require("../integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq");
/** /**
* Route to handle Vehicle Damage Estimate Add Request * Route to handle Vehicle Damage Estimate Add Request
@@ -38,7 +38,7 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_
* Route to handle Vehicle Damage Estimate Change Request * Route to handle Vehicle Damage Estimate Change Request
*/ */
router.post( router.post(
"/parts-management/VehicleDamageEstimateChgRq", "/parts-management/VehicleDamageEstimateChqRq",
bodyParser.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body bodyParser.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body
partsManagementIntegrationMiddleware, partsManagementIntegrationMiddleware,
partsManagementVehicleDamageEstimateChqRq partsManagementVehicleDamageEstimateChqRq