Compare commits

...

9 Commits

Author SHA1 Message Date
Dave
71c6d9fa94 IO-3473 trim user input 2025-12-19 12:07:30 -05:00
Allan Carr
6ea1c291e6 Merged in release/2025-12-19 (pull request #2703)
Release/2025 12 19
2025-12-12 02:36:57 +00:00
Allan Carr
05d5c96491 Merged in feature/IO-3462-Project-Mexico-Mod (pull request #2701)
IO-3462 Project Mexico Mod

Approved-by: Dave Richer
2025-12-10 20:18:14 +00:00
Allan Carr
35a566cbe5 IO-3462 Project Mexico Mod
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-12-10 09:52:04 -08:00
Dave Richer
f12e40e4c6 Merged in feature/IO-3461-Fix-EULA (pull request #2698)
feature/IO-3461-Fix-Eula
2025-12-09 22:15:22 +00:00
Dave
bb4e671c83 feature/IO-3461-Fix-Eula 2025-12-09 17:13:59 -05:00
Dave Richer
d1637d2432 Merged in release/2025-12-05 (pull request #2696)
Release/2025 12 05 into master-AIO - IO-3450 IO-3452 IO-3262 - IO-3456 IO-3262
2025-12-06 01:48:37 +00:00
Allan Carr
1c79628613 Merged in feature/IO-3262-Tech-Console-Job-Clock-Out (pull request #2692)
IO-3262 Correction for v_year in Project Mexico

Approved-by: Dave Richer
2025-12-05 19:00:38 +00:00
Allan Carr
521a7084b7 IO-3262 Correction for v_year in Project Mexico
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-12-05 09:48:32 -08:00
5 changed files with 164 additions and 103 deletions

View File

@@ -138,7 +138,7 @@ export function App({
);
}
if (currentEula && !currentUser.eulaIsAccepted) {
if (!isPartsEntry && currentEula && !currentUser.eulaIsAccepted) {
return <Eula />;
}

View File

@@ -55,7 +55,8 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => {
const useremail = currentUser.email;
try {
const { ...otherFormValues } = formValues;
// eslint-disable-next-line no-unused-vars
const { accepted_terms, ...otherFormValues } = formValues;
// Trim the values of the fields before submitting
const trimmedFormValues = Object.entries(otherFormValues).reduce((acc, [key, value]) => {

View File

@@ -117,44 +117,46 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
imexshopid: shopid,
json: JSON.stringify(carfaxObject, null, 2),
filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
count: carfaxObject.job.length
count: carfaxObject?.job?.length || 0
};
if (skipUpload) {
fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
uploadToS3(jsonObj, S3_BUCKET_NAME);
} else {
await uploadViaSFTP(jsonObj);
if (jsonObj.count > 0) {
await uploadViaSFTP(jsonObj);
await sendMexicoBillingEmail({
subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
text: `Errors:\n${JSON.stringify(
erroredJobs.map((ej) => ({
jobid: ej.job?.id,
error: ej.error
})),
null,
2
)}\n\nUploaded:\n${JSON.stringify(
{
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
},
null,
2
)}`
});
await sendMexicoBillingEmail({
subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
text: `Errors:\n${JSON.stringify(
erroredJobs.map((ej) => ({
jobid: ej.job?.id,
error: ej.error
})),
null,
2
)}\n\nUploaded:\n${JSON.stringify(
{
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
},
null,
2
)}`
});
}
}
allJSONResults.push({
jsonObj.count > 0 && allJSONResults.push({
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
result: jsonObj.result || "No Upload Result Available"
});
logger.log("CARFAX-RPS-end-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -234,11 +236,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
const ret = {
ro_number: crypto.createHash("md5").update(job.id, "utf8").digest("hex"),
v_vin: job.v_vin || "",
v_year: job.v_model_yr
? parseInt(job.v_model_yr.match(/\d/g))
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
: ""
: "",
v_year: (() => {
const y = parseInt(job.v_model_yr);
return isNaN(y) ? null : y < 100 ? y + (y >= (new Date().getFullYear() + 1) % 100 ? 1900 : 2000) : y;
})(),
v_make: job.v_makedesc || "",
v_model: job.v_model || "",

View File

@@ -160,40 +160,42 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
imexshopid: shopid,
json: JSON.stringify(carfaxObject, null, 2),
filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
count: carfaxObject.job.length
count: carfaxObject?.job?.length || 0
};
if (skipUpload) {
fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
uploadToS3(jsonObj);
} else {
await uploadViaSFTP(jsonObj);
if (jsonObj.count > 0) {
await uploadViaSFTP(jsonObj);
await sendMexicoBillingEmail({
subject: `${shopid.replace(/_/g, "").toUpperCase()}_Mexico${InstanceManager({
imex: "IO",
rome: "RO"
})}_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
text: `Errors:\n${JSON.stringify(
erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
})),
null,
2
)}\n\nUploaded:\n${JSON.stringify(
{
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
},
null,
2
)}`
});
await sendMexicoBillingEmail({
subject: `${shopid.replace(/_/g, "").toUpperCase()}_Mexico${InstanceManager({
imex: "IO",
rome: "RO"
})}_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
text: `Errors:\n${JSON.stringify(
erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
})),
null,
2
)}\n\nUploaded:\n${JSON.stringify(
{
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
},
null,
2
)}`
});
}
}
allJSONResults.push({
@@ -201,7 +203,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
imexshopid: shopid,
count: jsonObj.count,
filename: jsonObj.filename,
result: jsonObj.result
result: jsonObj.result || "No Upload Result Available"
});
logger.log("CARFAX-end-shop-extract", "DEBUG", "api", bodyshop.id, {
@@ -286,11 +288,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
const ret = {
ro_number: crypto.createHash("md5").update(job.ro_number, "utf8").digest("hex"),
v_vin: job.v_vin || "",
v_year: job.v_model_yr
? parseInt(job.v_model_yr.match(/\d/g))
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
: ""
: "",
v_year: (() => {
const y = parseInt(job.v_model_yr);
return isNaN(y) ? null : y < 100 ? y + (y >= (new Date().getFullYear() + 1) % 100 ? 1900 : 2000) : y;
})(),
v_make: job.v_make_desc || "",
v_model: job.v_model_desc || "",

View File

@@ -77,9 +77,8 @@ const generateResetLink = async (email) => {
*/
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.` };
}
return !!resp.bodyshops.length;
};
/**
@@ -225,10 +224,25 @@ const patchPartsManagementProvisioning = async (req, res) => {
*/
const partsManagementProvisioning = async (req, res) => {
const { logger } = req;
const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
// Trim and normalize email early
const body = {
...req.body,
userEmail: req.body.userEmail?.trim().toLowerCase()
};
const trim = (value) => (typeof value === "string" ? value.trim() : value);
const trimIfString = (value) =>
value !== null && value !== undefined && typeof value === "string" ? value.trim() : value;
try {
// Ensure email is present and trimmed before checking registration
if (!body.userEmail) {
throw { status: 400, message: "userEmail is required" };
}
await ensureEmailNotRegistered(body.userEmail);
requireFields(body, [
"external_shop_id",
"shopname",
@@ -241,27 +255,69 @@ const partsManagementProvisioning = async (req, res) => {
"phone",
"userEmail"
]);
await ensureExternalIdUnique(body.external_shop_id);
logger.log("admin-create-shop-user", "debug", body.userEmail, null, {
// Trim all top-level string fields
const trimmedBody = {
...body,
external_shop_id: trim(body.external_shop_id),
shopname: trim(body.shopname),
address1: trim(body.address1),
address2: trimIfString(body.address2),
city: trim(body.city),
state: trim(body.state),
zip_post: trim(body.zip_post),
country: trim(body.country),
email: trim(body.email),
phone: trim(body.phone),
timezone: trimIfString(body.timezone),
logoUrl: trimIfString(body.logoUrl),
userPassword: body.userPassword, // passwords should NOT be trimmed (preserves intentional spaces if any, though rare)
vendors: Array.isArray(body.vendors)
? body.vendors.map((v) => ({
name: trim(v.name),
street1: trimIfString(v.street1),
street2: trimIfString(v.street2),
city: trimIfString(v.city),
state: trimIfString(v.state),
zip: trimIfString(v.zip),
country: trimIfString(v.country),
email: trimIfString(v.email),
cost_center: trimIfString(v.cost_center),
phone: trimIfString(v.phone),
dmsid: trimIfString(v.dmsid),
discount: v.discount ?? 0,
due_date: v.due_date ?? null,
favorite: v.favorite ?? [],
active: v.active ?? true
}))
: []
};
const duplicateCheck = await ensureExternalIdUnique(trimmedBody.external_shop_id);
if (duplicateCheck) {
throw { status: 400, message: `external_shop_id '${trimmedBody.external_shop_id}' is already in use.` };
}
logger.log("admin-create-shop-user", "debug", trimmedBody.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,
shopname: trimmedBody.shopname,
address1: trimmedBody.address1,
address2: trimmedBody.address2,
city: trimmedBody.city,
state: trimmedBody.state,
zip_post: trimmedBody.zip_post,
country: trimmedBody.country,
email: trimmedBody.email,
external_shop_id: trimmedBody.external_shop_id,
timezone: trimmedBody.timezone || DefaultNewShop.timezone,
phone: trimmedBody.phone,
logo_img_path: {
src: body.logoUrl,
src: trimmedBody.logoUrl || null, // allow empty logo
width: "",
height: "",
headerMargin: DefaultNewShop.logo_img_path.headerMargin
@@ -286,35 +342,37 @@ const partsManagementProvisioning = async (req, res) => {
appt_alt_transport: DefaultNewShop.appt_alt_transport,
md_jobline_presets: DefaultNewShop.md_jobline_presets,
vendors: {
data: body.vendors.map((v) => ({
data: trimmedBody.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
street1: v.street1,
street2: v.street2,
city: v.city,
state: v.state,
zip: v.zip,
country: v.country,
email: v.email,
discount: v.discount,
due_date: v.due_date,
cost_center: v.cost_center,
favorite: v.favorite,
phone: v.phone,
active: v.active,
dmsid: v.dmsid
}))
}
};
const newShopId = await insertBodyshop(shopInput);
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
const userRecord = await createFirebaseUser(trimmedBody.userEmail, trimmedBody.userPassword);
let resetLink = null;
if (!body.userPassword) resetLink = await generateResetLink(body.userEmail);
if (!trimmedBody.userPassword) {
resetLink = await generateResetLink(trimmedBody.userEmail);
}
const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId);
const createdUser = await insertUserAssociation(userRecord.uid, trimmedBody.userEmail, newShopId);
return res.status(200).json({
shop: { id: newShopId, shopname: body.shopname },
shop: { id: newShopId, shopname: trimmedBody.shopname },
user: {
id: createdUser.id,
email: createdUser.email,
@@ -322,7 +380,7 @@ const partsManagementProvisioning = async (req, res) => {
}
});
} catch (err) {
logger.log("admin-create-shop-user-error", "error", body.userEmail, null, {
logger.log("admin-create-shop-user-error", "error", body.userEmail || "unknown", null, {
message: err.message,
detail: err.detail || err
});