Merged in feature/IO-3255-simplified-part-management (pull request #2534)

feature/IO-3255-simplified-parts-management - Fix deprovision route
This commit is contained in:
Dave Richer
2025-09-03 14:47:51 +00:00

View File

@@ -1,6 +1,5 @@
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 { const {
DELETE_SHOP, DELETE_SHOP,
DELETE_VENDORS_BY_SHOP, DELETE_VENDORS_BY_SHOP,
@@ -18,145 +17,154 @@ const {
/** /**
* Deletes a Firebase user by UID. * Deletes a Firebase user by UID.
* @param uid * @param {string} uid - The Firebase user ID
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const deleteFirebaseUser = async (uid) => { const deleteFirebaseUser = async (uid) => {
if (!uid) throw new Error("User UID is required");
return admin.auth().deleteUser(uid); return admin.auth().deleteUser(uid);
}; };
/** /**
* Deletes all vendors associated with a shop. * Deletes all vendors associated with a shop.
* @param shopId * @param {string} shopId - The shop ID
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const deleteVendorsByShop = async (shopId) => { const deleteVendorsByShop = async (shopId) => {
if (!shopId) throw new Error("Shop ID is required");
await client.request(DELETE_VENDORS_BY_SHOP, { shopId }); await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
}; };
/** /**
* Deletes a bodyshop from the database. * Deletes a bodyshop from the database.
* @param shopId * @param {string} shopId - The shop ID
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const deleteBodyshop = async (shopId) => { const deleteBodyshop = async (shopId) => {
if (!shopId) throw new Error("Shop ID is required");
await client.request(DELETE_SHOP, { id: shopId }); await client.request(DELETE_SHOP, { id: shopId });
}; };
/** /**
* Fetch job ids for a given shop * Fetch job ids for a given shop
* @param shopId * @param {string} shopId - The shop ID
* @returns {Promise<string[]>} * @returns {Promise<string[]>}
*/ */
const getJobIdsForShop = async (shopId) => { const getJobIdsForShop = async (shopId) => {
if (!shopId) throw new Error("Shop ID is required");
const resp = await client.request(GET_JOBS_BY_SHOP, { shopId }); 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 * Delete joblines for the given job ids
* @param jobIds {string[]} * @param {string[]} jobIds - Array of job IDs
* @returns {Promise<number>} affected rows * @returns {Promise<number>} affected rows
*/ */
const deleteJoblinesForJobs = async (jobIds) => { 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 }); 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 * Delete jobs for the given job ids
* @param jobIds {string[]} * @param {string[]} jobIds - Array of job IDs
* @returns {Promise<number>} affected rows * @returns {Promise<number>} affected rows
*/ */
const deleteJobsByIds = async (jobIds) => { const deleteJobsByIds = async (jobIds) => {
if (!jobIds.length) return 0; if (!jobIds?.length) return 0;
const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds }); 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. * Handles deprovisioning a shop for parts management.
* @param req * @param {Object} req - Express request object
* @param res * @param {Object} res - Express response object
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
const partsManagementDeprovisioning = async (req, res) => { const partsManagementDeprovisioning = async (req, res) => {
const { logger } = req; const { logger } = req;
const body = req.body; const { shopId } = req.body;
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." }); return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
} }
try { try {
if (!body.shopId) { if (!shopId) {
throw { status: 400, message: "shopId is required." }; throw { status: 400, message: "shopId is required." };
} }
// Fetch bodyshop and check external_shop_id // 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; const shop = shopResp.bodyshops_by_pk;
if (!shop) { 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) { if (!shop.external_shop_id) {
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." }; throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
} }
logger.log("admin-delete-shop", "debug", null, null, { logger.log("admin-delete-shop", "debug", null, null, {
shopId: body.shopId, shopId,
shopname: shop.shopname, shopname: shop.shopname,
ioadmin: true ioadmin: true
}); });
// Get vendors // Get vendors
const vendorsResp = await client.request(GET_VENDORS, { shopId: body.shopId }); const vendorsResp = await client.request(GET_VENDORS, { shopId });
const deletedVendors = vendorsResp.vendors.map((v) => v.name); const deletedVendors = vendorsResp.vendors?.map((v) => v.name) || [];
// Get associated users // Get associated users
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: body.shopId }); const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId });
const associatedUsers = assocResp.associations.map((assoc) => ({ const associatedUsers =
authId: assoc.user.authid, assocResp.associations?.map((assoc) => ({
email: assoc.user.email authId: assoc.user?.authid,
})); email: assoc.user?.email
})) || [];
// Delete associations for the shop // Delete associations for the shop
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: body.shopId }); const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId });
const associationsDeleted = assocDeleteResp.delete_associations.affected_rows; 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 = []; const deletedUsers = [];
for (const user of associatedUsers) { for (const user of associatedUsers) {
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email }); if (!user.email || !user.authId) continue;
// Determine which users now have zero associations and should be deleted (defer deletion until end) try {
const emailsToAuthId = associatedUsers.reduce((acc, u) => { const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
acc[u.email] = u.authId; const assocCount = countResp.associations_aggregate?.aggregate?.count || 0;
return acc; if (assocCount === 0) {
}, {}); await client.request(DELETE_USER, { email: user.email });
const emailsToDelete = []; await deleteFirebaseUser(user.authId);
await client.request(DELETE_USER, { email: user.email }); deletedUsers.push(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 joblinesDeleted = await deleteJoblinesForJobs(jobIds);
const jobsDeleted = await deleteJobsByIds(jobIds); const jobsDeleted = await deleteJobsByIds(jobIds);
// Delete any audit trail entries tied to this bodyshop to avoid FK violations // Delete audit trail
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: body.shopId }); const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId });
const auditDeleted = auditResp.delete_audit_trail.affected_rows; const auditDeleted = auditResp.delete_audit_trail?.affected_rows || 0;
// Delete vendors // Delete vendors and shop
await deleteVendorsByShop(body.shopId); await deleteVendorsByShop(shopId);
await deleteBodyshop(shopId);
// Delete shop
await deleteBodyshop(body.shopId);
// Summary log // Summary log
logger.log("admin-delete-shop-summary", "info", null, null, { logger.log("admin-delete-shop-summary", "info", null, null, {
shopId: body.shopId, shopId,
shopname: shop.shopname, shopname: shop.shopname,
associationsDeleted, associationsDeleted,
deletedUsers, deletedUsers,
@@ -167,31 +175,20 @@ const partsManagementDeprovisioning = async (req, res) => {
}); });
return res.status(200).json({ return res.status(200).json({
message: `Bodyshop ${body.shopId} and associated resources deleted successfully.`, message: `Bodyshop ${shopId} and associated resources deleted successfully.`,
deletedShop: { id: body.shopId, name: shop.shopname }, deletedShop: { id: shopId, name: shop.shopname },
deletedAssociationsCount: associationsDeleted, deletedAssociationsCount: associationsDeleted,
deletedUsers: deletedUsers, deletedUsers,
deletedVendors: deletedVendors, deletedVendors,
deletedJoblinesCount: joblinesDeleted, deletedJoblinesCount: joblinesDeleted,
deletedJobsCount: jobsDeleted, deletedJobsCount: jobsDeleted,
deletedAuditTrailCount: auditDeleted 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) { } catch (err) {
logger.log("admin-delete-shop-error", "error", null, null, { logger.log("admin-delete-shop-error", "error", null, null, {
message: err.message, 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" }); return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
} }
}; };