diff --git a/server/integrations/partsManagement/partsManagementProvisioning.js b/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js similarity index 59% rename from server/integrations/partsManagement/partsManagementProvisioning.js rename to server/integrations/partsManagement/endpoints/partsManagementProvisioning.js index 8c8d2a394..797ce619d 100644 --- a/server/integrations/partsManagement/partsManagementProvisioning.js +++ b/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js @@ -1,12 +1,15 @@ const admin = require("firebase-admin"); -const client = require("../../graphql-client/graphql-client").client; -const DefaultNewShop = require("./defaultNewShop.json"); +const client = require("../../../graphql-client/graphql-client").client; +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) => { for (const field of fields) { 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} - */ const ensureEmailNotRegistered = async (email) => { try { 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} - */ const createFirebaseUser = async (email, password = null) => { const userData = { email }; - if (password) { - userData.password = password; - } + if (password) userData.password = password; return admin.auth().createUser(userData); }; -/** - * Deletes a Firebase user by their UID. - * @param uid - * @returns {Promise} - */ const deleteFirebaseUser = async (uid) => { return admin.auth().deleteUser(uid); }; -/** - * Generates a password reset link for the given email. - * @param email - * @returns {Promise} - */ const generateResetLink = async (email) => { return admin.auth().generatePasswordResetLink(email); }; -/** - * Ensures that the external shop ID is unique in the database. - * @param externalId - * @returns {Promise} - */ const ensureExternalIdUnique = async (externalId) => { - const query = ` - query CHECK_KEY($key: String!) { - bodyshops(where: { external_shop_id: { _eq: $key } }) { - external_shop_id - } - }`; - const resp = await client.request(query, { key: externalId }); + const resp = await client.request(CHECK_EXTERNAL_SHOP_ID, { key: externalId }); if (resp.bodyshops.length) { 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 mutation = ` - mutation CREATE_SHOP($bs: bodyshops_insert_input!) { - insert_bodyshops_one(object: $bs) { id } - }`; - const resp = await client.request(mutation, { bs: input }); + const resp = await client.request(CREATE_SHOP, { bs: input }); return resp.insert_bodyshops_one.id; }; -/** - * Deletes all vendors associated with a specific shop ID. - * @param shopId - * @returns {Promise} - */ const deleteVendorsByShop = async (shopId) => { - const mutation = ` - mutation DELETE_VENDORS($shopId: uuid!) { - delete_vendors(where: { shopid: { _eq: $shopId } }) { - affected_rows - } - }`; - await client.request(mutation, { shopId }); + await client.request(DELETE_VENDORS_BY_SHOP, { shopId }); }; -/** - * Deletes a bodyshop by its ID. - * @param shopId - * @returns {Promise} - */ const deleteBodyshop = async (shopId) => { - const mutation = ` - mutation DELETE_SHOP($id: uuid!) { - delete_bodyshops_by_pk(id: $id) { id } - }`; - await client.request(mutation, { id: shopId }); + await client.request(DELETE_SHOP, { 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 mutation = ` - mutation CREATE_USER($u: users_insert_input!) { - insert_users_one(object: $u) { - id: authid - email - } - }`; const vars = { u: { 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; }; -/** - * Handles the provisioning of a new parts management shop and user. - * @param req - * @param res - * @returns {Promise<*>} - */ const partsManagementProvisioning = async (req, res) => { const { logger } = req; const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; try { - // Validate inputs await ensureEmailNotRegistered(p.userEmail); requireFields(p, [ "external_shop_id", @@ -184,7 +103,6 @@ const partsManagementProvisioning = async (req, res) => { ioadmin: true }); - // Create shop const shopInput = { shopname: p.shopname, address1: p.address1, @@ -204,7 +122,7 @@ const partsManagementProvisioning = async (req, res) => { headerMargin: DefaultNewShop.logo_img_path.headerMargin }, features: { - partsManagementOnly: true // This is a parts management only shop + partsManagementOnly: true }, md_ro_statuses: DefaultNewShop.md_ro_statuses, 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); let resetLink = null; - if (!p.userPassword) { - resetLink = await generateResetLink(p.userEmail); - } + if (!p.userPassword) resetLink = await generateResetLink(p.userEmail); + const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId); return res.status(200).json({ @@ -242,7 +158,7 @@ const partsManagementProvisioning = async (req, res) => { user: { id: createdUser.id, email: createdUser.email, - resetLink: resetLink || undefined // Only include resetLink if it exists + resetLink: resetLink || undefined } }); } catch (err) { @@ -251,18 +167,17 @@ const partsManagementProvisioning = async (req, res) => { detail: err.detail || err }); - // Cleanup on failure if (err.userRecord) { await deleteFirebaseUser(err.userRecord.uid).catch(() => { - // Ignore errors during user deletion cleanup + /* empty */ }); } if (err.newShopId) { await deleteVendorsByShop(err.newShopId).catch(() => { - // Ignore errors during vendor deletion cleanup + /* empty */ }); await deleteBodyshop(err.newShopId).catch(() => { - // Ignore errors during shop deletion cleanup + /* empty */ }); } diff --git a/server/integrations/partsManagement/vehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js similarity index 99% rename from server/integrations/partsManagement/vehicleDamageEstimateAddRq.js rename to server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js index 82bf09cc5..c0ff0fa34 100644 --- a/server/integrations/partsManagement/vehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq.js @@ -1,8 +1,8 @@ // no-dd-sa:javascript-code-style/assignment-name // CamelCase is used for GraphQL and database fields. -const client = require("../../graphql-client/graphql-client").client; -const { parseXml, normalizeXmlObject } = require("./partsManagementUtils"); +const client = require("../../../graphql-client/graphql-client").client; +const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); // GraphQL Queries and Mutations const { @@ -10,7 +10,7 @@ const { GET_VEHICLE_BY_SHOP_VIN, INSERT_OWNER, INSERT_JOB_WITH_LINES -} = require("./partsManagement.queries"); +} = require("../partsManagement.queries"); // Defaults const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; diff --git a/server/integrations/partsManagement/vehicleDamageEstimateChgRq.js b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js similarity index 79% rename from server/integrations/partsManagement/vehicleDamageEstimateChgRq.js rename to server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js index a486bd24a..936a901da 100644 --- a/server/integrations/partsManagement/vehicleDamageEstimateChgRq.js +++ b/server/integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq.js @@ -1,19 +1,19 @@ // no-dd-sa:javascript-code-style/assignment-name // Handler for VehicleDamageEstimateChgRq -const client = require("../../graphql-client/graphql-client").client; -const { parseXml, normalizeXmlObject } = require("./partsManagementUtils"); +const client = require("../../../graphql-client/graphql-client").client; +const { parseXml, normalizeXmlObject } = require("../partsManagementUtils"); const { - GET_JOB_BY_CLAIM_OR_DOCID, - UPDATE_JOB_BY_PK, + GET_JOB_BY_CLAIM, + UPDATE_JOB_BY_ID, UPSERT_JOBLINES, DELETE_JOBLINES_BY_IDS -} = require("./partsManagement.queries"); +} = require("../partsManagement.queries"); -const findJob = async (claimNum, documentId, logger) => { +const findJob = async (shopId, claimNum, logger) => { 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; } catch (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 || []]; return lines.map((line) => ({ + jobid: jobId, line_no: parseInt(line.LineNum, 10), unq_seq: parseInt(line.UniqueSequenceNum, 10), status: line.LineStatusCode || null, @@ -51,7 +52,7 @@ const extractUpdatedJobLines = (addsChgs = {}) => { lbr_op: line.LaborInfo?.LaborOperation || null, lbr_amt: parseFloat(line.LaborInfo?.LaborAmt || 0), 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 claimNum = rq.ClaimInfo?.ClaimNum; - const documentId = rq.DocumentInfo?.DocumentID; 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"); const updatedJobData = extractUpdatedJobData(rq); - const updatedLines = extractUpdatedJobLines(rq.AddsChgs); + const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id); 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) { await client.request(UPSERT_JOBLINES, { - jobid: job.id, joblines: updatedLines }); } diff --git a/server/integrations/partsManagement/partsManagement.queries.js b/server/integrations/partsManagement/partsManagement.queries.js index e0ba6088c..c56e82493 100644 --- a/server/integrations/partsManagement/partsManagement.queries.js +++ b/server/integrations/partsManagement/partsManagement.queries.js @@ -53,34 +53,50 @@ const UPDATE_JOB_BY_ID = ` `; const UPSERT_JOBLINES = ` - mutation UpsertJoblines($joblines: [joblines_insert_input!]!) { - insert_joblines( - objects: $joblines - on_conflict: { - constraint: joblines_jobid_line_no_unq_seq_key - update_columns: [ - status - line_desc - part_type - part_qty - oem_partno - db_price - act_price - mod_lbr_ty - mod_lb_hrs - lbr_op - lbr_amt - notes - ] - } - ) { +mutation UpsertJoblines($joblines: [joblines_insert_input!]!) { + insert_joblines( + objects: $joblines + on_conflict: { + constraint: joblines_pkey + update_columns: [ + jobid + status + line_desc + part_type + part_qty + oem_partno + db_price + act_price + mod_lbr_ty + mod_lb_hrs + lbr_op + lbr_amt + notes + manual_line + ] + } + ) { + affected_rows + } +} +`; + +const DELETE_JOBLINES_BY_JOBID = ` + mutation DeleteJoblinesByJobId($jobid: uuid!) { + delete_joblines(where: { jobid: { _eq: $jobid } }) { affected_rows } } `; -const DELETE_JOBLINES_BY_JOBID = ` - mutation DeleteJoblinesByJobId($jobid: uuid!) { - delete_joblines(where: { jobid: { _eq: $jobid } }) { + +const DELETE_JOBLINES_BY_IDS = ` + mutation DeleteJoblinesByIds($jobid: uuid!, $unqSeqs: [Int!]!) { + delete_joblines( + where: { + jobid: { _eq: $jobid }, + unq_seq: { _in: $unqSeqs } + } + ) { 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 = { GET_BODYSHOP_STATUS, GET_VEHICLE_BY_SHOP_VIN, @@ -101,7 +154,13 @@ module.exports = { INSERT_JOB_WITH_LINES, GET_JOB_BY_CLAIM, UPDATE_JOB_BY_ID, - DELETE_JOBLINES_BY_JOBID, 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 }; diff --git a/server/routes/intergrationRoutes.js b/server/routes/intergrationRoutes.js index 5914a2bde..d51f46b58 100644 --- a/server/routes/intergrationRoutes.js +++ b/server/routes/intergrationRoutes.js @@ -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) { 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 partsManagementVehicleDamageEstimateAddRq = require("../integrations/partsManagement/vehicleDamageEstimateAddRq"); - const partsManagementVehicleDamageEstimateChqRq = require("../integrations/partsManagement/vehicleDamageEstimateChgRq"); + const partsManagementVehicleDamageEstimateAddRq = require("../integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq"); + const partsManagementVehicleDamageEstimateChqRq = require("../integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq"); /** * 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 */ router.post( - "/parts-management/VehicleDamageEstimateChgRq", + "/parts-management/VehicleDamageEstimateChqRq", bodyParser.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body partsManagementIntegrationMiddleware, partsManagementVehicleDamageEstimateChqRq