diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index 649551ff6..2c84e1ee7 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -345,13 +345,13 @@ export function* SetAuthLevelFromShopDetails({ payload }) { payload.features?.allAccess === true ? window.$crisp.push(["set", "session:segments", [["allAccess"]]]) : (() => { - const featureKeys = Object.keys(payload.features).filter( - (key) => - payload.features[key] === true || - (typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key]))) - ); - window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]); - })(); + const featureKeys = Object.keys(payload.features).filter( + (key) => + payload.features[key] === true || + (typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key]))) + ); + window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]); + })(); InstanceRenderManager({ executeFunction: true, @@ -361,8 +361,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) { } }); - //Set whether it is for parts management only. + //Set whether it is for parts management only. + // TODO: This is a temp fix we do not want to do a check on true yield put(setPartsManagementOnly(true || payload.features.partsManagementOnly)); } catch (error) { console.warn("Couldnt find $crisp.", error.message); diff --git a/server/integrations/partsManagement/partsManagementProvisioning.js b/server/integrations/partsManagement/partsManagementProvisioning.js index 4fe5970e9..8c8d2a394 100644 --- a/server/integrations/partsManagement/partsManagementProvisioning.js +++ b/server/integrations/partsManagement/partsManagementProvisioning.js @@ -32,12 +32,17 @@ const ensureEmailNotRegistered = async (email) => { }; /** - * Creates a new Firebase user with the provided email. + * Creates a new Firebase user with the provided email and optional password. * @param email + * @param password * @returns {Promise} */ -const createFirebaseUser = async (email) => { - return admin.auth().createUser({ email }); +const createFirebaseUser = async (email, password = null) => { + const userData = { email }; + if (password) { + userData.password = password; + } + return admin.auth().createUser(userData); }; /** @@ -158,7 +163,7 @@ const partsManagementProvisioning = async (req, res) => { const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; try { - // Validate inputs + // Validate inputs await ensureEmailNotRegistered(p.userEmail); requireFields(p, [ "external_shop_id", @@ -179,7 +184,7 @@ const partsManagementProvisioning = async (req, res) => { ioadmin: true }); - // Create shop + // Create shop const shopInput = { shopname: p.shopname, address1: p.address1, @@ -198,6 +203,9 @@ const partsManagementProvisioning = async (req, res) => { height: "", headerMargin: DefaultNewShop.logo_img_path.headerMargin }, + features: { + partsManagementOnly: true // This is a parts management only shop + }, md_ro_statuses: DefaultNewShop.md_ro_statuses, vendors: { data: p.vendors.map((v) => ({ @@ -221,9 +229,12 @@ const partsManagementProvisioning = async (req, res) => { }; const newShopId = await insertBodyshop(shopInput); - // Create user + association - const userRecord = await createFirebaseUser(p.userEmail); - const resetLink = await generateResetLink(p.userEmail); + // Create user + association + 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({ @@ -231,7 +242,7 @@ const partsManagementProvisioning = async (req, res) => { user: { id: createdUser.id, email: createdUser.email, - resetLink + resetLink: resetLink || undefined // Only include resetLink if it exists } }); } catch (err) { diff --git a/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js b/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js index ea3803725..547b4739f 100644 --- a/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js +++ b/server/integrations/partsManagement/partsManagementVehicleDamageEstimateAddRq.js @@ -9,11 +9,7 @@ const { GET_BODYSHOP_STATUS, GET_VEHICLE_BY_SHOP_VIN, INSERT_OWNER, - INSERT_JOB_WITH_LINES, - GET_JOB_BY_CLAIM, - UPDATE_JOB_BY_ID, - INSERT_JOBLINES, - DELETE_JOBLINES_BY_JOBID + INSERT_JOB_WITH_LINES } = require("./partsManagement.queries"); // Defaults @@ -177,12 +173,28 @@ const extractJobData = (rq) => { asgn_no: asgn.AssignmentNumber || null, asgn_type: asgn.AssignmentType || null, asgn_date: asgn.AssignmentDate || null, + // asgn_created: asgn.CreateDateTime || null, scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null, scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null, clm_no: ci.ClaimNum || null, status: ci.ClaimStatus || null, policy_no: ci.PolicyInfo?.PolicyNum || null, ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0) + // document_id: doc.DocumentID || null, + // bms_version: doc.BMSVer || null, + // reference_info: + // doc.ReferenceInfo?.OtherReferenceInfo?.map((ref) => ({ + // name: ref.OtherReferenceName, + // number: ref.OtherRefNum + // })) || [], + // currency_info: doc.CurrencyInfo + // ? { + // code: doc.CurrencyInfo.CurCode, + // base_code: doc.CurrencyInfo.BaseCurCode, + // rate: parseFloat(doc.CurrencyInfo.CurRate || 0), + // rule: doc.CurrencyInfo.CurConvertRule + // } + // : null }; }; @@ -200,14 +212,12 @@ const extractJobData = (rq) => { * @returns {object} Owner data for insertion and inline use. */ const extractOwnerData = (rq, shopId) => { - // Prefer Owner, fallback to Claimant const ownerOrClaimant = rq.AdminInfo?.Owner?.Party || rq.AdminInfo?.Claimant?.Party || {}; - const personInfo = ownerOrClaimant.PersonInfo || {}; const personName = personInfo.PersonName || {}; const address = personInfo.Communications?.Address || {}; - let ownr_ph1, ownr_ph2, ownr_ea; + let ownr_ph1, ownr_ph2, ownr_ea, ownr_alt_ph; const comms = Array.isArray(ownerOrClaimant.ContactInfo?.Communications) ? ownerOrClaimant.ContactInfo.Communications @@ -217,6 +227,7 @@ const extractOwnerData = (rq, shopId) => { if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone; if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone; if (c.CommQualifier === "EM") ownr_ea = c.CommEmail; + if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone; } return { @@ -232,7 +243,11 @@ const extractOwnerData = (rq, shopId) => { ownr_ctry: address.Country || null, ownr_ph1, ownr_ph2, - ownr_ea + ownr_ea, + ownr_alt_ph + // ownr_id_qualifier: ownerOrClaimant.IDInfo?.IDQualifierCode || null // New + // ownr_id_num: ownerOrClaimant.IDInfo?.IDNum || null, // New + // ownr_preferred_contact: ownerOrClaimant.PreferredContactMethod || null // New }; }; @@ -304,13 +319,19 @@ const extractLossInfo = (rq) => { loss_date: loss.LossDateTime || null, loss_type: custom.LossTypeCode || null, loss_desc: custom.LossTypeDesc || null + // primary_poi: loss.PrimaryPOI?.POICode || null, + // secondary_poi: loss.SecondaryPOI?.POICode || null, + // damage_memo: loss.DamageMemo || null, //(maybe ins_memo) + // total_loss_ind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null // New }; }; /** * Extracts insurance data from the XML request. * @param rq - * @returns {{insd_ln: (*|null), insd_fn: (string|null), insd_title: (*|null), insd_co_nm: (*|string|null), insd_addr1: (*|null), insd_addr2: (*|null), insd_city: (*|null), insd_st: (*|null), insd_zip: (*|null), insd_ctry: (*|null), insd_ph1, insd_ph1x, insd_ph2, insd_ph2x, insd_fax, insd_faxx, insd_ea}} + * @returns {{insd_ln: (*|null), insd_fn: (string|null), insd_title: (*|null), insd_co_nm: (*|string|null), insd_addr1: + * (*|null), insd_addr2: (*|null), insd_city: (*|null), insd_st: (*|null), insd_zip: (*|null), insd_ctry: (*|null), + * insd_ph1, insd_ph1x, insd_ph2, insd_ph2x, insd_fax, insd_faxx, insd_ea}} */ const extractInsuranceData = (rq) => { const insuredParty = rq.AdminInfo?.Insured?.Party || {}; @@ -424,7 +445,10 @@ const extractJobLines = (rq) => { mod_lb_hrs: parseFloat(line.LaborInfo?.LaborHours || 0), lbr_op: line.LaborInfo?.LaborOperation || null, lbr_amt: parseFloat(line.LaborInfo?.LaborAmt || 0), - notes: line.LineMemo || null + notes: line.LineMemo || null, + manual_line: line.ManualLineInd || null + // taxable_ind: line.PartInfo?.TaxableInd || null, + // automated_entry: line.AutomatedEntry || null }; // TODO: Commented out as not clear if needed for version 1, this only applies to Imex and not rome on the front @@ -440,18 +464,6 @@ const extractJobLines = (rq) => { }); }; -/** - * Checks if the request is a supplement or a document portion delta. - * TODO: This is a temporary check, should be replaced with a proper field in the XML. - * @param rq - * @returns {boolean} - */ -const isSupplement = (rq) => { - const docStatus = rq.DocumentInfo?.DocumentStatus; - const historyType = rq.RepairTotalsHistory?.HistoryTotalType; - return docStatus === "S" || historyType === "DocumentPortionDelta"; -}; - /** * Finds an existing vehicle by shopId and VIN. * @param {string} shopId - The bodyshop UUID. @@ -474,26 +486,6 @@ const findExistingVehicle = async (shopId, v_vin, logger) => { return null; }; -/** - * Finds an existing job by shopid and claim number. - * @param shopid - * @param clm_no - * @param logger - * @returns {Promise<*|null>} - */ -const findExistingJob = async (shopid, clm_no, logger) => { - try { - const { jobs } = await client.request(GET_JOB_BY_CLAIM, { - shopid, - clm_no - }); - return jobs?.[0] || null; - } catch (err) { - logger.log("parts-job-fetch-failed", "warn", null, null, { error: err }); - return null; - } -}; - /** * Inserts an owner and returns the owner ID. * @param {object} ownerInput - The owner data to insert. @@ -568,21 +560,6 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { const joblinesData = extractJobLines(rq); const insuranceData = extractInsuranceData(rq); - // Uncomment for debugging - // console.dir( - // { - // joblinesData, - // lossInfo, - // insuranceData, - // vehicleData, - // ownerData, - // adjusterData, - // repairFacilityData, - // estimatorData - // }, - // { depth: null } - // ); - // Find or create relationships const ownerid = await insertOwner(ownerData, logger); const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger); @@ -628,36 +605,6 @@ const partsManagementVehicleDamageEstimateAddRq = async (req, res) => { joblines: { data: joblinesData } }; - // Check if this is a supplement or document portion delta. - if (isSupplement(rq)) { - console.log("----------------------IS SUPPLEMENT----------------------"); - const existingJob = await findExistingJob(shopId, clm_no, logger); - if (existingJob) { - const { joblines, ...jobWithoutLines } = jobInput; - - await client.request(UPDATE_JOB_BY_ID, { - id: existingJob.id, - job: jobWithoutLines - }); - - await client.request(DELETE_JOBLINES_BY_JOBID, { - jobid: existingJob.id - }); - - if (joblines?.data?.length) { - const joblinesWithJobId = joblines.data.map((line) => ({ - ...line, - jobid: existingJob.id - })); - - await client.request(INSERT_JOBLINES, { joblines: joblinesWithJobId }); - } - - logger.log("parts-job-updated", "info", existingJob.id); - return res.status(200).json({ success: true, jobId: existingJob.id }); - } - } - // Insert job const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput }); logger.log("parts-job-created", "info", newJob.id, null); diff --git a/server/integrations/partsManagement/swagger.yaml b/server/integrations/partsManagement/swagger.yaml index 40e28e9b5..03987f0f0 100644 --- a/server/integrations/partsManagement/swagger.yaml +++ b/server/integrations/partsManagement/swagger.yaml @@ -54,6 +54,10 @@ paths: userEmail: type: string format: email + userPassword: + type: string + description: Optional password for the new user. If provided, the password is set directly, and no password reset link is sent. Must be at least 6 characters. + nullable: true logoUrl: type: string format: uri @@ -140,6 +144,8 @@ paths: resetLink: type: string format: uri + nullable: true + description: Password reset link for the user. Only included if userPassword is not provided in the request. '400': description: Bad request (missing or invalid fields) content: