From e4fb8b61b0b38f3a052d04ea5030bfcafbafb2c0 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 25 Jul 2025 12:26:36 -0400 Subject: [PATCH] feature/IO-3255-simplified-parts-management - Deprovisoning route --- .../partsManagementDeprovisioning.js | 187 ++++++++++++++++++ server/routes/intergrationRoutes.js | 4 + 2 files changed, 191 insertions(+) create mode 100644 server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js diff --git a/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js b/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js new file mode 100644 index 000000000..084afef7d --- /dev/null +++ b/server/integrations/partsManagement/endpoints/partsManagementDeprovisioning.js @@ -0,0 +1,187 @@ +const admin = require("firebase-admin"); +const client = require("../../../graphql-client/graphql-client").client; + +const { DELETE_SHOP } = require("../partsManagement.queries"); + +// Define corrected DELETE_VENDORS_BY_SHOP locally +const DELETE_VENDORS_BY_SHOP = ` + mutation DELETE_VENDORS_BY_SHOP($shopId: uuid!) { + delete_vendors(where: {bodyshopid: {_eq: $shopId}}) { + affected_rows + } + } +`; + +// New queries for deprovisioning +const GET_BODYSHOP = ` + query GetBodyshop($id: uuid!) { + bodyshops_by_pk(id: $id) { + external_shop_id + shopname + } + } +`; + +const GET_ASSOCIATED_USERS = ` + query GetAssociatedUsers($shopId: uuid!) { + associations(where: {shopid: {_eq: $shopId}}) { + user { + authid + email + } + } + } +`; + +const DELETE_ASSOCIATIONS_BY_SHOP = ` + mutation DeleteAssociationsByShop($shopId: uuid!) { + delete_associations(where: {shopid: {_eq: $shopId}}) { + affected_rows + } + } +`; + +const GET_USER_ASSOCIATIONS_COUNT = ` + query GetUserAssociationsCount($userEmail: String!) { + associations_aggregate(where: {useremail: {_eq: $userEmail}}) { + aggregate { + count + } + } + } +`; + +const DELETE_USER = ` + mutation DeleteUser($email: String!) { + delete_users(where: {email: {_eq: $email}}) { + affected_rows + } + } +`; + +const GET_VENDORS = ` + query GetVendors($shopId: uuid!) { + vendors(where: {bodyshopid: {_eq: $shopId}}) { + name + } + } +`; + +/** + * Deletes a Firebase user by UID. + * @param uid + * @returns {Promise} + */ +const deleteFirebaseUser = async (uid) => { + return admin.auth().deleteUser(uid); +}; + +/** + * Deletes all vendors associated with a shop. + * @param shopId + * @returns {Promise} + */ +const deleteVendorsByShop = async (shopId) => { + await client.request(DELETE_VENDORS_BY_SHOP, { shopId }); +}; + +/** + * Deletes a bodyshop from the database. + * @param shopId + * @returns {Promise} + */ +const deleteBodyshop = async (shopId) => { + await client.request(DELETE_SHOP, { id: shopId }); +}; + +/** + * Handles deprovisioning a shop for parts management. + * @param req + * @param res + * @returns {Promise<*>} + */ +const partsManagementDeprovisioning = async (req, res) => { + const { logger } = req; + const p = req.body; + + if (process.env.NODE_ENV === "production") { + return res.status(403).json({ error: "Deprovisioning not allowed in production environment." }); + } + + try { + if (!p.shopId) { + throw { status: 400, message: "shopId is required." }; + } + + // Fetch bodyshop and check external_shop_id + const shopResp = await client.request(GET_BODYSHOP, { id: p.shopId }); + const shop = shopResp.bodyshops_by_pk; + if (!shop) { + throw { status: 404, message: `Bodyshop with id ${p.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: p.shopId, + shopname: shop.shopname, + ioadmin: true + }); + + // Get vendors + const vendorsResp = await client.request(GET_VENDORS, { shopId: p.shopId }); + const deletedVendors = vendorsResp.vendors.map((v) => v.name); + + // Get associated users + const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: p.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: p.shopId }); + const associationsDeleted = assocDeleteResp.delete_associations.affected_rows; + + // For each user, check if they have remaining associations; if not, delete user and Firebase account + const deletedUsers = []; + for (const user of associatedUsers) { + const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email }); + const assocCount = countResp.associations_aggregate.aggregate.count; + if (assocCount === 0) { + await client.request(DELETE_USER, { email: user.email }); + await deleteFirebaseUser(user.authId); + deletedUsers.push(user.email); + } + } + + // Delete vendors + await deleteVendorsByShop(p.shopId); + + // Delete shop + await deleteBodyshop(p.shopId); + + // Summary log + console.log( + `Deleted bodyshop ${p.shopId} (${shop.shopname}), ${associationsDeleted} associations, ${deletedUsers.length} users (${deletedUsers.join(", ") || "none"}), ${deletedVendors.length} vendors (${deletedVendors.join(", ") || "none"}).` + ); + + return res.status(200).json({ + message: `Bodyshop ${p.shopId} and associated resources deleted successfully.`, + deletedShop: { id: p.shopId, name: shop.shopname }, + deletedAssociationsCount: associationsDeleted, + deletedUsers: deletedUsers, + deletedVendors: deletedVendors + }); + } catch (err) { + logger.log("admin-delete-shop-error", "error", null, null, { + message: err.message, + detail: err.detail || err + }); + + return res.status(err.status || 500).json({ error: err.message || "Internal server error" }); + } +}; + +module.exports = partsManagementDeprovisioning; diff --git a/server/routes/intergrationRoutes.js b/server/routes/intergrationRoutes.js index 829809478..93c84d5b8 100644 --- a/server/routes/intergrationRoutes.js +++ b/server/routes/intergrationRoutes.js @@ -19,6 +19,7 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_ const XML_BODY_LIMIT = "10mb"; // Set a limit for XML body size const partsManagementProvisioning = require("../integrations/partsManagement/endpoints/partsManagementProvisioning"); + const partsManagementDeprovisioning = require("../integrations/partsManagement/endpoints/partsManagementDeprovisioning"); const partsManagementIntegrationMiddleware = require("../middleware/partsManagementIntegrationMiddleware"); const partsManagementVehicleDamageEstimateAddRq = require("../integrations/partsManagement/endpoints/vehicleDamageEstimateAddRq"); const partsManagementVehicleDamageEstimateChqRq = require("../integrations/partsManagement/endpoints/vehicleDamageEstimateChgRq"); @@ -43,6 +44,9 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_ partsManagementVehicleDamageEstimateChqRq ); + // Deprovisioning route + router.post("/parts-management/deprovision", partsManagementIntegrationMiddleware, partsManagementDeprovisioning); + /** * Route to handle Parts Management Provisioning */