diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 8d18dce03..c905978c4 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -26084,6 +26084,27 @@ + + additionalpayeroverallocation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + additionaltotal false @@ -28062,6 +28083,27 @@ + + multipayers + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + net_repairs false diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index 73da49047..d0746a92b 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -16,6 +16,7 @@ import DataLabel from "../data-label/data-label.component"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { useTreatments } from "@splitsoftware/splitio-react"; const SelectorDiv = styled.div` .ant-form-item .ant-select { @@ -37,6 +38,11 @@ export default connect( export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { const { t } = useTranslation(); + const { Qb_Multi_Ar } = useTreatments( + ["Qb_Multi_Ar"], + {}, + bodyshop && bodyshop.imexshopid + ); const [costOptions, setCostOptions] = useState( [ @@ -4535,24 +4541,26 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { - Multiple Payers Item}> - - - - + {Qb_Multi_Ar.treatment === "on" && ( + Multiple Payers Item}> + + + + + )} {t("bodyshop.labels.responsibilitycenters.sales_tax_codes")} diff --git a/client/src/graphql/audit_trail.queries.js b/client/src/graphql/audit_trail.queries.js index b0c0b3f40..793cc0001 100644 --- a/client/src/graphql/audit_trail.queries.js +++ b/client/src/graphql/audit_trail.queries.js @@ -26,6 +26,7 @@ export const QUERY_AUDIT_TRAIL = gql` subject to useremail + status } } `; diff --git a/client/src/pages/jobs-close/jobs-close.component.jsx b/client/src/pages/jobs-close/jobs-close.component.jsx index b8f73c144..3df8aafc6 100644 --- a/client/src/pages/jobs-close/jobs-close.component.jsx +++ b/client/src/pages/jobs-close/jobs-close.component.jsx @@ -12,7 +12,9 @@ import { Popconfirm, Select, Space, + Statistic, Switch, + Typography, } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -33,7 +35,7 @@ import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.qu import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; - +import Dinero from "dinero.js"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, jobRO: selectJobReadOnly, @@ -325,9 +327,41 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { )} + {t("jobs.labels.multipayers")} {Qb_Multi_Ar.treatment === "on" && ( - <> - + + ({ + validator(_, value) { + let totalAllocated = Dinero(); + + const payers = form.getFieldValue("qb_multiple_payers"); + payers && + payers.forEach((payer) => { + totalAllocated = totalAllocated.add( + Dinero({ + amount: Math.round((payer?.amount || 0) * 100), + }) + ); + }); + const discrep = job.job_totals + ? Dinero(job.job_totals.totals.total_repairs).subtract( + totalAllocated + ) + : Dinero(); + return discrep.getAmount() > 0 + ? Promise.resolve() + : Promise.reject( + new Error( + t("jobs.labels.additionalpayeroverallocation") + ) + ); + }, + }), + ]} + > {(fields, { add, remove }) => { return ( @@ -382,7 +416,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { { - if (fields.length < 3) add(); + add(); }} style={{ width: "100%" }} > @@ -393,7 +427,50 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { ); }} - > + + {() => { + //Perform Calculation to determine discrepancy. + let totalAllocated = Dinero(); + + const payers = form.getFieldValue("qb_multiple_payers"); + payers && + payers.forEach((payer) => { + totalAllocated = totalAllocated.add( + Dinero({ amount: Math.round((payer?.amount || 0) * 100) }) + ); + }); + const discrep = job.job_totals + ? Dinero(job.job_totals.totals.total_repairs).subtract( + totalAllocated + ) + : Dinero(); + return ( + + + - + + = + 0 ? "green" : "red", + }} + value={discrep.toFormat()} + /> + + ); + }} + + )} diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 3a63d470a..3570e04dc 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1546,6 +1546,7 @@ "actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).", "actual_delivery_inferred": "$t(jobs.fields.actual_delivery) inferred using $t(jobs.fields.scheduled_delivery).", "actual_in_inferred": "$t(jobs.fields.actual_in) inferred using $t(jobs.fields.scheduled_in).", + "additionalpayeroverallocation": "You have allocated more than the sale of the Job to additional payers.", "additionaltotal": "Additional Total", "adjustmentrate": "Adjustment Rate", "adjustments": "Adjustments", @@ -1649,6 +1650,7 @@ "mapa": "Paint Materials", "markforreexport": "Mark for Re-export", "mash": "Shop Materials", + "multipayers": "Additional Payers", "net_repairs": "Net Repairs", "notes": "Notes", "othertotal": "Other Totals", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index e1dcf6d16..919e650e9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1546,6 +1546,7 @@ "actual_completion_inferred": "", "actual_delivery_inferred": "", "actual_in_inferred": "", + "additionalpayeroverallocation": "", "additionaltotal": "", "adjustmentrate": "", "adjustments": "", @@ -1649,6 +1650,7 @@ "mapa": "", "markforreexport": "", "mash": "", + "multipayers": "", "net_repairs": "", "notes": "Notas", "othertotal": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 673f93cf4..82294d431 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1546,6 +1546,7 @@ "actual_completion_inferred": "", "actual_delivery_inferred": "", "actual_in_inferred": "", + "additionalpayeroverallocation": "", "additionaltotal": "", "adjustmentrate": "", "adjustments": "", @@ -1649,6 +1650,7 @@ "mapa": "", "markforreexport": "", "mash": "", + "multipayers": "", "net_repairs": "", "notes": "Remarques", "othertotal": "", diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index 057c6c042..a09c22d80 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -100,7 +100,9 @@ exports.default = async (req, res) => { oauthClient, qbo_realmId, req, - job + job, + isThreeTier, + insCoCustomerTier ); //Query for the owner itself. if (!ownerCustomerTier) { @@ -121,7 +123,11 @@ exports.default = async (req, res) => { qbo_realmId, req, job, - isThreeTier ? ownerCustomerTier : null // ownerCustomerTier || insCoCustomerTier + isThreeTier + ? ownerCustomerTier + : twoTierPref === "source" + ? insCoCustomerTier + : ownerCustomerTier ); // Need to validate that the job tier is associated to the right individual? @@ -342,7 +348,14 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) { } exports.InsertInsuranceCo = InsertInsuranceCo; -async function QueryOwner(oauthClient, qbo_realmId, req, job) { +async function QueryOwner( + oauthClient, + qbo_realmId, + req, + job, + isThreeTier, + parentTierRef +) { const ownerName = generateOwnerTier(job, true, null); const result = await oauthClient.makeApiCall({ url: urlBuilder( @@ -362,7 +375,9 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job) { result.json && result.json.QueryResponse && result.json.QueryResponse.Customer && - result.json.QueryResponse.Customer[0] + result.json.QueryResponse.Customer.find( + (x) => x.ParentRef?.value === parentTierRef?.Id + ) ); } exports.QueryOwner = QueryOwner; diff --git a/server/data/arms.js b/server/data/arms.js index 7e3cd9643..959c13382 100644 --- a/server/data/arms.js +++ b/server/data/arms.js @@ -205,8 +205,8 @@ exports.default = async (req, res) => { Party: { PersonInfo: { PersonName: { - FirstName: job.ownr_fn, - LastName: job.ownr_ln, + FirstName: job.ownr_co_nm ? "" : job.ownr_fn, + LastName: job.ownr_co_nm ? job.ownr_co_nm : job.ownr_ln, }, // Communications: [ // { @@ -336,7 +336,7 @@ exports.default = async (req, res) => { LossDateTime: job.loss_date && moment(job.loss_date) - .tz(bodyshop.timezone) + //.tz(bodyshop.timezone) .format(momentFormat), LossDescCode: "Collision", PrimaryPOI: { @@ -515,8 +515,11 @@ exports.default = async (req, res) => { { TotalType: "LAB", TotalTypeDesc: "Body Labor", - TotalHours: job.job_totals.rates.lab.hours, - TotalAmt: Dinero(job.job_totals.rates.lab.total).toFormat("0.00"), + TotalHours: + job.job_totals.rates.lab.hours + job.job_totals.rates.la1.hours, + TotalAmt: Dinero(job.job_totals.rates.lab.total) + .add(Dinero(job.job_totals.rates.la1.total)) + .toFormat("0.00"), }, { TotalType: "LAF", @@ -635,9 +638,9 @@ exports.default = async (req, res) => { { TotalType: "OTAC", TotalTypeDesc: "Additional Charges", - TotalAmt: Dinero( - job.job_totals.additional.additionalCosts - ).toFormat("0.00"), + TotalAmt: Dinero(job.job_totals.additional.additionalCosts) + .add(Dinero(job.job_totals.additional.pvrt)) + .toFormat("0.00"), }, ], SummaryTotalsInfo: [ diff --git a/server/email/sendemail.js b/server/email/sendemail.js index c585b4815..e3565d3e0 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -209,17 +209,15 @@ async function logEmail(req, email) { exports.emailBounce = async function (req, res, next) { try { const body = JSON.parse(req.body); - if (body.type === "SubscriptionConfirmation") { - logger.log("SNS-confirmation", "DEBUG", "api", null, { - message: body.message, - url: body.SubscribeUrl, - body: body, + if (body.Type === "SubscriptionConfirmation") { + logger.log("SNS-message", "DEBUG", "api", null, { + body: req.body, }); } - if (body.Type === "Notification") { - const message = JSON.parse(body.Message); + if (body.notificationType === "Bounce") { + // const message = JSON.parse(body.Message); let replyTo, subject, messageId; - message.mail.headers.forEach((header) => { + body.mail.headers.forEach((header) => { if (header.name === "Reply-To") { replyTo = header.value; } else if (header.name === "Subject") { @@ -228,26 +226,30 @@ exports.emailBounce = async function (req, res, next) { messageId = header.value; } }); + + if (replyTo === "noreply@imex.online") { + res.sendStatus(200); + return; + } //If it's bounced, log it as bounced in audit log. Send an email to the user. const result = await client.request(queries.UPDATE_EMAIL_AUDIT, { sesid: messageId, status: "Bounced", - context: message.bounce?.bouncedRecipients, + context: body.bounce?.bouncedRecipients, }); transporter.sendMail( { from: `ImEX Online `, - to: "patrick@snapt.ca", // replyTo, + to: replyTo, bcc: "patrick@snapt.ca", subject: `ImEX Online Bounced Email - RE: ${subject}`, - text: ` - ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. + text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. - ${message.bounce?.bouncedRecipients.map( - (r) => - `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} +${body.bounce?.bouncedRecipients.map( + (r) => + `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} ` - )} +)} `, }, (err, info) => {