const admin = require("firebase-admin"); const client = require("../../../graphql-client/graphql-client").client; const { DELETE_SHOP, DELETE_VENDORS_BY_SHOP, GET_BODYSHOP, GET_ASSOCIATED_USERS, DELETE_ASSOCIATIONS_BY_SHOP, GET_USER_ASSOCIATIONS_COUNT, DELETE_USER, GET_VENDORS, GET_JOBS_BY_SHOP, DELETE_JOBLINES_BY_JOB_IDS, DELETE_JOBS_BY_IDS, DELETE_AUDIT_TRAIL_BY_SHOP } = require("../partsManagement.queries"); /** * Deletes a Firebase user by 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 {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 {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 {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) || []; }; /** * Delete joblines for the given job ids * @param {string[]} jobIds - Array of job IDs * @returns {Promise} affected rows */ const deleteJoblinesForJobs = async (jobIds) => { if (!jobIds?.length) return 0; const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds }); return resp.delete_joblines?.affected_rows || 0; }; /** * Delete jobs for the given job ids * @param {string[]} jobIds - Array of job IDs * @returns {Promise} affected rows */ const deleteJobsByIds = async (jobIds) => { if (!jobIds?.length) return 0; const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds }); return resp.delete_jobs?.affected_rows || 0; }; /** * Handles deprovisioning a shop for parts management. * @param {Object} req - Express request object * @param {Object} res - Express response object * @returns {Promise<*>} */ const partsManagementDeprovisioning = async (req, res) => { const { logger } = req; const { shopId } = req.body; if (process.env.NODE_ENV === "production" || process.env.HOSTNAME?.endsWith("compute.internal")) { return res.status(403).json({ error: "Deprovisioning not allowed in production environment." }); } try { if (!shopId) { throw { status: 400, message: "shopId is required." }; } // Fetch bodyshop and check external_shop_id const shopResp = await client.request(GET_BODYSHOP, { id: shopId }); const shop = shopResp.bodyshops_by_pk; if (!shop) { 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, shopname: shop.shopname, ioadmin: true }); // Get vendors 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 }); 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 }); const associationsDeleted = assocDeleteResp.delete_associations?.affected_rows || 0; // Delete users with no remaining associations const deletedUsers = []; for (const user of associatedUsers) { 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 || userError }); } } // Delete jobs and joblines const jobIds = await getJobIdsForShop(shopId); const joblinesDeleted = await deleteJoblinesForJobs(jobIds); const jobsDeleted = await deleteJobsByIds(jobIds); // 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 and shop await deleteVendorsByShop(shopId); await deleteBodyshop(shopId); // Summary log logger.log("admin-delete-shop-summary", "info", null, null, { shopId, shopname: shop.shopname, associationsDeleted, deletedUsers, deletedVendors, joblinesDeleted, jobsDeleted, auditDeleted }); return res.status(200).json({ message: `Bodyshop ${shopId} and associated resources deleted successfully.`, deletedShop: { id: shopId, name: shop.shopname }, deletedAssociationsCount: associationsDeleted, deletedUsers, deletedVendors, deletedJoblinesCount: joblinesDeleted, deletedJobsCount: jobsDeleted, deletedAuditTrailCount: auditDeleted }); } catch (err) { logger.log("admin-delete-shop-error", "error", null, null, { message: err.message, detail: err.detail || err.stack || err }); return res.status(err.status || 500).json({ error: err.message || "Internal server error" }); } }; module.exports = partsManagementDeprovisioning;