From f3535c01af0b75bd86d9480a00646e57263b0776 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 3 Sep 2025 10:47:04 -0400 Subject: [PATCH] feature/IO-3255-simplified-parts-management - Fix deprovision route --- .../partsManagementDeprovisioning.js | 129 +++++++++--------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js b/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js index 449023a86..d780daaa5 100644 --- a/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js +++ b/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js @@ -1,6 +1,5 @@ const admin = require("firebase-admin"); const client = require("../../../graphql-client/graphql-client").client; - const { DELETE_SHOP, DELETE_VENDORS_BY_SHOP, @@ -18,145 +17,154 @@ const { /** * Deletes a Firebase user by UID. - * @param uid + * @param {string} uid - The Firebase user ID * @returns {Promise} */ const deleteFirebaseUser = async (uid) => { + if (!uid) throw new Error("User UID is required"); return admin.auth().deleteUser(uid); }; /** * Deletes all vendors associated with a shop. - * @param shopId + * @param {string} shopId - The shop ID * @returns {Promise} */ const deleteVendorsByShop = async (shopId) => { + if (!shopId) throw new Error("Shop ID is required"); await client.request(DELETE_VENDORS_BY_SHOP, { shopId }); }; /** * Deletes a bodyshop from the database. - * @param shopId + * @param {string} shopId - The shop ID * @returns {Promise} */ const deleteBodyshop = async (shopId) => { + if (!shopId) throw new Error("Shop ID is required"); await client.request(DELETE_SHOP, { id: shopId }); }; /** * Fetch job ids for a given shop - * @param shopId + * @param {string} shopId - The shop ID * @returns {Promise} */ const getJobIdsForShop = async (shopId) => { + if (!shopId) throw new Error("Shop ID is required"); const resp = await client.request(GET_JOBS_BY_SHOP, { shopId }); - return resp.jobs.map((j) => j.id); + return resp.jobs?.map((j) => j.id) || []; }; /** * Delete joblines for the given job ids - * @param jobIds {string[]} + * @param {string[]} jobIds - Array of job IDs * @returns {Promise} affected rows */ const deleteJoblinesForJobs = async (jobIds) => { - if (!jobIds.length) return 0; + if (!jobIds?.length) return 0; const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds }); - return resp.delete_joblines.affected_rows; + return resp.delete_joblines?.affected_rows || 0; }; /** * Delete jobs for the given job ids - * @param jobIds {string[]} + * @param {string[]} jobIds - Array of job IDs * @returns {Promise} affected rows */ const deleteJobsByIds = async (jobIds) => { - if (!jobIds.length) return 0; + if (!jobIds?.length) return 0; const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds }); - return resp.delete_jobs.affected_rows; + return resp.delete_jobs?.affected_rows || 0; }; /** * Handles deprovisioning a shop for parts management. - * @param req - * @param res + * @param {Object} req - Express request object + * @param {Object} res - Express response object * @returns {Promise<*>} */ const partsManagementDeprovisioning = async (req, res) => { const { logger } = req; - const body = req.body; + const { shopId } = req.body; if (process.env.NODE_ENV === "production") { return res.status(403).json({ error: "Deprovisioning not allowed in production environment." }); } try { - if (!body.shopId) { + if (!shopId) { throw { status: 400, message: "shopId is required." }; } // Fetch bodyshop and check external_shop_id - const shopResp = await client.request(GET_BODYSHOP, { id: body.shopId }); + const shopResp = await client.request(GET_BODYSHOP, { id: shopId }); const shop = shopResp.bodyshops_by_pk; if (!shop) { - throw { status: 404, message: `Bodyshop with id ${body.shopId} not found.` }; + throw { status: 404, message: `Bodyshop with id ${shopId} not found.` }; } if (!shop.external_shop_id) { throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." }; } logger.log("admin-delete-shop", "debug", null, null, { - shopId: body.shopId, + shopId, shopname: shop.shopname, ioadmin: true }); // Get vendors - const vendorsResp = await client.request(GET_VENDORS, { shopId: body.shopId }); - const deletedVendors = vendorsResp.vendors.map((v) => v.name); + const vendorsResp = await client.request(GET_VENDORS, { shopId }); + const deletedVendors = vendorsResp.vendors?.map((v) => v.name) || []; // Get associated users - const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: body.shopId }); - const associatedUsers = assocResp.associations.map((assoc) => ({ - authId: assoc.user.authid, - email: assoc.user.email - })); + const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId }); + const associatedUsers = + assocResp.associations?.map((assoc) => ({ + authId: assoc.user?.authid, + email: assoc.user?.email + })) || []; // Delete associations for the shop - const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: body.shopId }); - const associationsDeleted = assocDeleteResp.delete_associations.affected_rows; + const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId }); + const associationsDeleted = assocDeleteResp.delete_associations?.affected_rows || 0; - // For each user, check if they have remaining associations; if not, delete user and Firebase account + // Delete users with no remaining associations const deletedUsers = []; for (const user of associatedUsers) { - const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email }); - // Determine which users now have zero associations and should be deleted (defer deletion until end) - const emailsToAuthId = associatedUsers.reduce((acc, u) => { - acc[u.email] = u.authId; - return acc; - }, {}); - const emailsToDelete = []; - await client.request(DELETE_USER, { email: user.email }); - await deleteFirebaseUser(user.authId); - deletedUsers.push(user.email); + if (!user.email || !user.authId) continue; + try { + const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email }); + const assocCount = countResp.associations_aggregate?.aggregate?.count || 0; + if (assocCount === 0) { + await client.request(DELETE_USER, { email: user.email }); + await deleteFirebaseUser(user.authId); + deletedUsers.push(user.email); + } + } catch (userError) { + logger.log("admin-delete-user-error", "warn", null, null, { + email: user.email, + error: userError.message + }); + } } - emailsToDelete.push(user.email); - const jobIds = await getJobIdsForShop(body.shopId); + + // Delete jobs and joblines + const jobIds = await getJobIdsForShop(shopId); const joblinesDeleted = await deleteJoblinesForJobs(jobIds); const jobsDeleted = await deleteJobsByIds(jobIds); - // Delete any audit trail entries tied to this bodyshop to avoid FK violations - const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: body.shopId }); - const auditDeleted = auditResp.delete_audit_trail.affected_rows; + // Delete audit trail + const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId }); + const auditDeleted = auditResp.delete_audit_trail?.affected_rows || 0; - // Delete vendors - await deleteVendorsByShop(body.shopId); - - // Delete shop - await deleteBodyshop(body.shopId); + // Delete vendors and shop + await deleteVendorsByShop(shopId); + await deleteBodyshop(shopId); // Summary log logger.log("admin-delete-shop-summary", "info", null, null, { - shopId: body.shopId, + shopId, shopname: shop.shopname, associationsDeleted, deletedUsers, @@ -167,31 +175,20 @@ const partsManagementDeprovisioning = async (req, res) => { }); return res.status(200).json({ - message: `Bodyshop ${body.shopId} and associated resources deleted successfully.`, - deletedShop: { id: body.shopId, name: shop.shopname }, + message: `Bodyshop ${shopId} and associated resources deleted successfully.`, + deletedShop: { id: shopId, name: shop.shopname }, deletedAssociationsCount: associationsDeleted, - deletedUsers: deletedUsers, - deletedVendors: deletedVendors, + deletedUsers, + deletedVendors, deletedJoblinesCount: joblinesDeleted, deletedJobsCount: jobsDeleted, deletedAuditTrailCount: auditDeleted }); - // Now delete users that have no remaining associations and their Firebase accounts - const deletedUsers = []; - for (const email of emailsToDelete) { - await client.request(DELETE_USER, { email }); - const authId = emailsToAuthId[email]; - if (authId) { - await deleteFirebaseUser(authId); - } - deletedUsers.push(email); - } } catch (err) { logger.log("admin-delete-shop-error", "error", null, null, { message: err.message, - detail: err.detail || err + detail: err.detail || err.stack || err }); - return res.status(err.status || 500).json({ error: err.message || "Internal server error" }); } };