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 } = 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: 80, active: true }] } } }; const resp = await client.request(CREATE_USER, vars); return resp.insert_users_one; }; /** * Handles provisioning a new shop for parts management. * @param req * @param res * @returns {Promise<*>} */ const partsManagementProvisioning = async (req, res) => { const { logger } = req; const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; try { await ensureEmailNotRegistered(p.userEmail); requireFields(p, [ "external_shop_id", "shopname", "address1", "city", "state", "zip_post", "country", "email", "phone", "userEmail" ]); await ensureExternalIdUnique(p.external_shop_id); logger.log("admin-create-shop-user", "debug", p.userEmail, null, { request: req.body, ioadmin: true }); const shopInput = { shopname: p.shopname, address1: p.address1, address2: p.address2 || null, city: p.city, state: p.state, zip_post: p.zip_post, country: p.country, email: p.email, external_shop_id: p.external_shop_id, timezone: p.timezone || DefaultNewShop.timezone, phone: p.phone, logo_img_path: { src: p.logoUrl, width: "", height: "", headerMargin: DefaultNewShop.logo_img_path.headerMargin }, features: { allAccess: true, // TODO: should be 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, // TODO need? md_categories: DefaultNewShop.md_categories, // TODO need? md_labor_rates: DefaultNewShop.md_labor_rates, // TODO need? md_payment_types: DefaultNewShop.md_payment_types, // TODO need? md_hour_split: DefaultNewShop.md_hour_split, // TODO need? md_ccc_rates: DefaultNewShop.md_ccc_rates, // TODO need? appt_alt_transport: DefaultNewShop.appt_alt_transport, // TODO need? md_jobline_presets: DefaultNewShop.md_jobline_presets, // TODO need? vendors: { data: p.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(p.userEmail, p.userPassword); let resetLink = null; if (!p.userPassword) resetLink = await generateResetLink(p.userEmail); const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId); return res.status(200).json({ shop: { id: newShopId, shopname: p.shopname }, user: { id: createdUser.id, email: createdUser.email, resetLink: resetLink || undefined } }); } catch (err) { logger.log("admin-create-shop-user-error", "error", p.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;