351 lines
9.6 KiB
JavaScript
351 lines
9.6 KiB
JavaScript
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<void>}
|
|
*/
|
|
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<UserRecord>}
|
|
*/
|
|
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<void>}
|
|
*/
|
|
const deleteFirebaseUser = async (uid) => {
|
|
return admin.auth().deleteUser(uid);
|
|
};
|
|
|
|
/**
|
|
* Generates a password reset link for the given email.
|
|
* @param email
|
|
* @returns {Promise<string>}
|
|
*/
|
|
const generateResetLink = async (email) => {
|
|
return admin.auth().generatePasswordResetLink(email);
|
|
};
|
|
|
|
/**
|
|
* Ensures that the provided external shop ID is unique.
|
|
* @param externalId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
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<void>}
|
|
*/
|
|
const deleteVendorsByShop = async (shopId) => {
|
|
await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
|
|
};
|
|
|
|
/**
|
|
* Deletes a bodyshop from the database.
|
|
* @param shopId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
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
|
|
* Also allows updating logo_img_path via a simple logoUrl string, which is expanded to the full object.
|
|
* @param req
|
|
* @param res
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const patchPartsManagementProvisioning = async (req, res) => {
|
|
const { id } = req.params;
|
|
|
|
// Fields that can be directly patched 1:1
|
|
const allowedFields = [
|
|
"shopname",
|
|
"address1",
|
|
"address2",
|
|
"city",
|
|
"state",
|
|
"zip_post",
|
|
"country",
|
|
"email",
|
|
"timezone",
|
|
"phone"
|
|
// NOTE: logo_img_path is handled separately via logoUrl
|
|
];
|
|
|
|
const updateFields = {};
|
|
|
|
// Copy over simple scalar fields if present
|
|
for (const field of allowedFields) {
|
|
if (req.body[field] !== undefined) {
|
|
updateFields[field] = req.body[field];
|
|
}
|
|
}
|
|
|
|
// Handle logo update via a simple href string, same behavior as provision route
|
|
if (typeof req.body.logo_img_path === "string") {
|
|
const trimmed = req.body.logo_img_path.trim();
|
|
if (trimmed) {
|
|
updateFields.logo_img_path = {
|
|
src: trimmed,
|
|
width: "",
|
|
height: "",
|
|
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
|
};
|
|
}
|
|
}
|
|
|
|
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 {
|
|
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"
|
|
]);
|
|
|
|
// TODO add in check for early access
|
|
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 };
|