const admin = require("firebase-admin"); 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, UPDATE_BODYSHOP_BY_ID } = require("../partsManagement.queries"); /** * Checks if the required fields are present in the payload. * @param payload * @param fields */ const requireFields = (payload, fields) => { for (const field of fields) { if (!payload[field]) { throw { status: 400, message: `${field} is required.` }; } } }; /** * Ensures that the provided email is not already registered in Firebase. * @param email * @returns {Promise} */ const ensureEmailNotRegistered = async (email) => { try { await admin.auth().getUserByEmail(email); throw { status: 400, message: "userEmail is already registered in Firebase." }; } catch (err) { if (err.code !== "auth/user-not-found") { throw { status: 500, message: "Error validating userEmail uniqueness", detail: err }; } } }; /** * Creates a new Firebase user with the given email and optional password. * @param email * @param password * @returns {Promise} */ const createFirebaseUser = async (email, password = null) => { const userData = { email }; if (password) userData.password = password; return admin.auth().createUser(userData); }; /** * Deletes a Firebase user by 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 provided external shop ID is unique. * @param externalId * @returns {Promise} */ const ensureExternalIdUnique = async (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 resp = await client.request(CREATE_SHOP, { bs: input }); return resp.insert_bodyshops_one.id; }; /** * 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 }); }; /** * Inserts a new user association into the database. * @param uid * @param email * @param shopId * @returns {Promise<*>} */ const insertUserAssociation = async (uid, email, shopId) => { const vars = { u: { email, authid: uid, validemail: true, associations: { data: [{ shopid: shopId, authlevel: 99, active: true }] } } }; const resp = await client.request(CREATE_USER, vars); return resp.insert_users_one; }; /** * PATCH handler for updating bodyshop fields. * Allows patching: shopname, address1, address2, city, state, zip_post, country, email, timezone, phone, logo_img_path * @param req * @param res * @returns {Promise} */ const patchPartsManagementProvisioning = async (req, res) => { const { id } = req.params; const allowedFields = [ "shopname", "address1", "address2", "city", "state", "zip_post", "country", "email", "timezone", "phone", "logo_img_path" ]; const updateFields = {}; for (const field of allowedFields) { if (req.body[field] !== undefined) { updateFields[field] = req.body[field]; } } if (Object.keys(updateFields).length === 0) { return res.status(400).json({ error: "No valid fields provided for update." }); } // Check that the bodyshop has an external_shop_id before allowing patch try { // Fetch the bodyshop by id const shopResp = await client.request( `query GetBodyshop($id: uuid!) { bodyshops_by_pk(id: $id) { id external_shop_id } }`, { id } ); if (!shopResp.bodyshops_by_pk?.external_shop_id) { return res.status(400).json({ error: "Cannot patch: bodyshop does not have an external_shop_id." }); } } catch (err) { return res.status(500).json({ error: "Failed to validate bodyshop external_shop_id.", detail: err }); } try { const resp = await client.request(UPDATE_BODYSHOP_BY_ID, { id, fields: updateFields }); if (!resp.update_bodyshops_by_pk) { return res.status(404).json({ error: "Bodyshop not found." }); } return res.json(resp.update_bodyshops_by_pk); } catch (err) { return res.status(500).json({ error: "Failed to update bodyshop.", detail: err }); } }; /** * Handles provisioning a new shop for parts management. * @param req * @param res * @returns {Promise<*>} */ const partsManagementProvisioning = async (req, res) => { const { logger } = req; const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; try { await ensureEmailNotRegistered(body.userEmail); requireFields(body, [ "external_shop_id", "shopname", "address1", "city", "state", "zip_post", "country", "email", "phone", "userEmail" ]); await ensureExternalIdUnique(body.external_shop_id); logger.log("admin-create-shop-user", "debug", body.userEmail, null, { request: req.body, ioadmin: true }); const shopInput = { shopname: body.shopname, address1: body.address1, address2: body.address2 || null, city: body.city, state: body.state, zip_post: body.zip_post, country: body.country, email: body.email, external_shop_id: body.external_shop_id, timezone: body.timezone || DefaultNewShop.timezone, phone: body.phone, logo_img_path: { src: body.logoUrl, width: "", height: "", headerMargin: DefaultNewShop.logo_img_path.headerMargin }, features: { allAccess: false, partsManagementOnly: true }, md_ro_statuses: DefaultNewShop.md_ro_statuses, md_order_statuses: DefaultNewShop.md_order_statuses, md_responsibility_centers: DefaultNewShop.md_responsibility_centers, md_referral_sources: DefaultNewShop.md_referral_sources, md_messaging_presets: DefaultNewShop.md_messaging_presets, md_rbac: DefaultNewShop.md_rbac, md_classes: DefaultNewShop.md_classes, md_ins_cos: DefaultNewShop.md_ins_cos, md_categories: DefaultNewShop.md_categories, md_labor_rates: DefaultNewShop.md_labor_rates, md_payment_types: DefaultNewShop.md_payment_types, md_hour_split: DefaultNewShop.md_hour_split, md_ccc_rates: DefaultNewShop.md_ccc_rates, appt_alt_transport: DefaultNewShop.appt_alt_transport, md_jobline_presets: DefaultNewShop.md_jobline_presets, vendors: { data: body.vendors.map((v) => ({ name: v.name, street1: v.street1 || null, street2: v.street2 || null, city: v.city || null, state: v.state || null, zip: v.zip || null, country: v.country || null, email: v.email || null, discount: v.discount ?? 0, due_date: v.due_date ?? null, cost_center: v.cost_center || null, favorite: v.favorite ?? [], phone: v.phone || null, active: v.active ?? true, dmsid: v.dmsid || null })) } }; const newShopId = await insertBodyshop(shopInput); const userRecord = await createFirebaseUser(body.userEmail, body.userPassword); let resetLink = null; if (!body.userPassword) resetLink = await generateResetLink(body.userEmail); const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId); return res.status(200).json({ shop: { id: newShopId, shopname: body.shopname }, user: { id: createdUser.id, email: createdUser.email, resetLink: resetLink || undefined } }); } catch (err) { logger.log("admin-create-shop-user-error", "error", body.userEmail, null, { message: err.message, detail: err.detail || err }); if (err.userRecord) { await deleteFirebaseUser(err.userRecord.uid).catch(() => { /* empty */ }); } if (err.newShopId) { await deleteVendorsByShop(err.newShopId).catch(() => { /* empty */ }); await deleteBodyshop(err.newShopId).catch(() => { /* empty */ }); } return res.status(err.status || 500).json({ error: err.message || "Internal server error" }); } }; module.exports = { partsManagementProvisioning, patchPartsManagementProvisioning };