Compare commits

...

17 Commits

Author SHA1 Message Date
Allan Carr
587f4a4492 IO-1366 Extend Audit Log
Extend for Time Ticket Changes, Manual Lines, Manual Job, Bill Edits

Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-04-01 22:51:05 -07:00
Allan Carr
c0a37d7c1a IO-1366 Bill Reexport Audit Log
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-31 09:50:08 -07:00
Dave
6759bc5865 release/2026-04-03 - Remove console.dir 2026-03-30 17:24:42 -04:00
Patrick Fic
04732fc6cd Merged in feature/IO-3356-rc-ford-ro-updates (pull request #3166)
IO-3356 Add CSR and Shop values per Grant @ RC Ford.

Approved-by: Dave Richer
2026-03-30 19:08:16 +00:00
Patrick Fic
a65a34ef1f Merged in feature/IO-3515-maintain-discount-on-ai-vendor (pull request #3167)
IO-3515 Retain discount application when AI vendor added.

Approved-by: Dave Richer
2026-03-30 19:07:43 +00:00
Patrick Fic
1ea7798eeb IO-3515 Retain discount application when AI vendor added. 2026-03-30 11:59:08 -07:00
Patrick Fic
7739d48741 IO-3356 Add CSR and Shop values per Grant @ RC Ford. 2026-03-30 11:44:38 -07:00
Allan Carr
074be66b8c Merged in feature/IO-3629-PostBatchWip-rtn-1-Catch (pull request #3163)
IO-3629 PostBatchWip Rtn != 0 error

Approved-by: Dave Richer
2026-03-30 15:07:15 +00:00
Allan Carr
8db8744782 IO-3629 PostBatchWip Rtn != 0 error
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-27 15:38:27 -07:00
Dave Richer
c2d8d78e0a Merged in hotfix/2026-03-27 (pull request #3162)
Hotfix/2026 03 27
2026-03-27 19:16:16 +00:00
Dave Richer
71aec6d0c5 Merged in hotfix/2026-03-27 (pull request #3159)
Hotfix/2026 03 27
2026-03-27 18:48:24 +00:00
Dave
f89d7865fa Restore hasura metadata tables from master-AIO 2026-03-27 14:46:00 -04:00
Dave
8fd368ebb4 Revert hasura metadata tables changes 2026-03-27 14:45:01 -04:00
Dave
132fc0a20f hotfix/2026-03-27 - Missing chatter stuff. 2026-03-27 14:36:37 -04:00
Allan Carr
9ea2d83043 Merged in feature/IO-3599-Taxable-Amount (pull request #3155)
IO-3599 Taxable Amount

Approved-by: Dave Richer
2026-03-25 22:36:08 +00:00
Allan Carr
abad7d5f00 Merged in feature/IO-3627-Courtesy-Car-Create-RO (pull request #3156)
IO-3627 Courtesy Car Create RO

Approved-by: Dave Richer
2026-03-25 22:35:33 +00:00
Allan Carr
c97213bc96 IO-3599 Taxable Amount
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-03-24 18:12:04 -07:00
16 changed files with 300 additions and 41 deletions

View File

@@ -134,11 +134,96 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
await Promise.all(updates);
const details = (() => {
const original = data?.bills_by_pk ?? {};
const updated = { ...bill, billlines };
const fmtVal = (key, val) =>
val == null || val === ""
? "<<empty>>"
: typeof val === "number" && key.toLowerCase().includes("price")
? `$${val.toFixed(2)}`
: val;
const keysToTrack = ["line_desc", "quantity", "actual_price", "actual_cost", "cost_center"];
const lineVals = (obj) => keysToTrack.map((k) => fmtVal(k, obj[k])).join(", ");
const changed = Object.entries(updated)
.filter(([k, v]) => v != null && v !== "" && k !== "billlines" && k !== "__typename")
.map(([k, v]) => {
const orig = original[k];
if (k === "date") {
const a = orig ? dayjs(orig) : null;
const b = v ? (dayjs.isDayjs(v) ? v : dayjs(v)) : null;
return (a && b && a.isSame(b, "day")) || (!a && !b)
? null
: `date: ${a?.format("YYYY-MM-DD") ?? "<<empty>>"}${b?.format("YYYY-MM-DD") ?? "<<empty>>"}`;
}
return typeof orig === "object" || typeof v === "object" || String(orig) === String(v)
? null
: `${k}: ${fmtVal(k, orig)}${fmtVal(k, v)}`;
})
.filter(Boolean);
const origLines = original.billlines ?? [];
const updLines = updated.billlines ?? [];
const addedObjs = updLines
.filter((l) => !l.id)
.map((u) => ({ label: u.line_desc || u.description || "new line", vals: lineVals(u), handled: false }));
const removedObjs = origLines
.filter((o) => !updLines.some((u) => u.id === o.id))
.map((o) => ({
label: o.line_desc || o.description || o.id || "removed line",
vals: lineVals(o),
handled: false
}));
const labelToAdded = addedObjs.reduce((m, a) => m.set(a.label, [...(m.get(a.label) ?? []), a]), new Map());
const modified = [
...removedObjs.reduce((acc, r) => {
const candidates = labelToAdded.get(r.label) ?? [];
const exact = candidates.find((c) => c.vals === r.vals && !c.handled);
if (exact) {
exact.handled = r.handled = true;
return acc;
} // identical → cancel out
const diff = candidates.find((c) => c.vals !== r.vals && !c.handled);
if (diff) {
diff.handled = r.handled = true;
acc.push(`${r.label}: ${r.vals}${diff.vals}`);
}
return acc;
}, []),
...updLines
.filter((u) => u.id)
.flatMap((u) => {
const o = origLines.find((x) => x.id === u.id);
if (!o) return [];
const diffs = keysToTrack
.filter((k) => String(o[k]) !== String(u[k]))
.map((k) => `${fmtVal(k, o[k])}${fmtVal(k, u[k])}`);
return diffs.length ? [`${u.line_desc || u.description || u.id}: ${diffs.join("; ")}`] : [];
})
];
[
["added", addedObjs.filter((a) => !a.handled).map((a) => `+${a.label} (${a.vals})`)],
["removed", removedObjs.filter((r) => !r.handled).map((r) => `-${r.label} (${r.vals})`)],
["modified", modified]
].forEach(([type, items]) => {
if (items.length) changed.push(`billlines ${type}: ${items.join(" | ")}`);
});
return changed.length ? changed.join("; ") : bill.invoice_number || "No changes";
})();
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated"
operation: AuditTrailMapping.billupdated(bill.invoice_number, details)
});
await refetch();

View File

@@ -52,6 +52,7 @@ export function BillFormComponent({
const [discount, setDiscount] = useState(0);
const notification = useNotification();
const jobIdFormWatch = Form.useWatch("jobid", form);
const vendorIdFormWatch = Form.useWatch("vendorid", form);
const {
treatments: { Extended_Bill_Posting, ClosingPeriod }
@@ -118,6 +119,7 @@ export function BillFormComponent({
}
}, [
form,
vendorIdFormWatch,
billEdit,
loadOutstandingReturns,
loadInventory,

View File

@@ -9,18 +9,20 @@ import { createStructuredSelector } from "reselect";
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { insertAuditTrail } from "../../redux/application/application.actions.js";
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
authLevel: selectAuthLevel
});
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(BillMarkForReexportButton);
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
export function BillMarkForReexportButton({ bodyshop, authLevel, bill, insertAuditTrail }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const notification = useNotification();
@@ -47,6 +49,12 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
notification.success({
title: t("bills.successes.reexport")
});
insertAuditTrail({
jobid: bill.jobid,
billid: bill.id,
operation: AuditTrailMapping.billmarkforreexport(bill.invoice_number),
type: "billmarkforreexport"
});
} else {
notification.error({
title: t("bills.errors.saving", {

View File

@@ -14,16 +14,19 @@ import CriticalPartsScan from "../../utils/criticalPartsScan";
import UndefinedToNull from "../../utils/undefinedtonull";
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
import { insertAuditTrail } from "../../redux/application/application.actions.js";
const mapStateToProps = createStructuredSelector({
jobLineEditModal: selectJobLineEditModal,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit"))
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop }) {
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
const {
treatments: { CriticalPartsScanning }
} = useTreatmentsWithConfig({
@@ -74,6 +77,16 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
notification.success({
title: t("joblines.successes.created")
});
insertAuditTrail({
jobid: jobLineEditModal.context.jobid,
operation: AuditTrailMapping.jobmanuallineinsert(
Object.entries(values)
.filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p")
.map(([k, v]) => `${k}: ${v}`)
.join("; ")
),
type: "jobmanuallineinsert"
});
} else {
notification.error({
title: t("joblines.errors.creating", {
@@ -103,6 +116,29 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
notification.success({
title: t("joblines.successes.updated")
});
insertAuditTrail({
jobid: jobLineEditModal.context.jobid,
operation: AuditTrailMapping.joblineupdate(
(() => {
const original = jobLineEditModal.context || {};
const changed = Object.entries(values)
.filter(([k, v]) => v != null && v !== "" && k !== "ah_detail_line" && k !== "prt_dsmk_p")
.map(([k, v]) => {
const orig = original[k];
if (String(orig) === String(v)) return null;
const fmt = (key, val) => {
if (val == null || val === "") return "<<empty>>";
if (typeof val === "number" && key.toLowerCase().includes("price")) return `$${val.toFixed(2)}`;
return val;
};
return `${k}: ${fmt(k, orig)}${fmt(k, v)}`;
})
.filter(Boolean);
return changed.length ? changed.join("; ") : "No changes";
})()
),
type: "joblineupdate"
});
} else {
notification.success({
title: t("joblines.errors.updating", {

View File

@@ -2,7 +2,7 @@ import { PageHeader } from "@ant-design/pro-layout";
import { useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Form, Modal, Space } from "antd";
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -15,21 +15,26 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from "../../utils/day";
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
import TimeTicketModalComponent from "./time-ticket-modal.component";
import { insertAuditTrail } from "../../redux/application/application.actions.js";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
timeTicketModal: selectTimeTicket,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket"))
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket")),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop }) {
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [enterAgain, setEnterAgain] = useState(false);
const lastSubmittedRef = useRef(null);
const [lineTicketRefreshKey, setLineTicketRefreshKey] = useState(0);
const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET);
@@ -50,6 +55,8 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
});
const handleFinish = (values) => {
// Save submitted values so we can compute audit-trail details after the mutation completes
lastSubmittedRef.current = values;
setLoading(true);
const emps = EmployeeAutoCompleteData?.employees.filter((e) => e.id === values.employeeid);
if (timeTicketModal.context.id) {
@@ -89,6 +96,44 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
title: t("timetickets.successes.created")
});
const timeticket = timeTicketModal.context?.timeticket ?? {};
const original = timeticket || {};
const submitted = lastSubmittedRef.current || {};
const fmt = (key, val) => {
if (val == null || val === "") return "<<empty>>";
const k = key.toLowerCase();
if (dayjs.isDayjs?.(val)) return dayjs(val).format(k.includes("clock") ? "YYYY-MM-DD HH:mm" : "YYYY-MM-DD");
if (typeof val === "number")
return k.includes("hrs")
? val.toFixed(1)
: k.includes("rate") || k.includes("price")
? `$${val.toFixed(2)}`
: String(val);
if (key === "employeeid") {
const emp = EmployeeAutoCompleteData?.employees?.find(({ id }) => id === val);
return emp ? `${emp.first_name} ${emp.last_name}` : String(val);
}
return String(val);
};
const changed = Object.entries(submitted)
.filter(([, v]) => v != null && v !== "")
.map(([k, v]) => {
const origVal = k === "jobid" ? (original.job?.id ?? original.jobid ?? original[k]) : original[k];
return String(fmt(k, origVal)) !== String(fmt(k, v)) ? `${k}: ${fmt(k, origVal)}${fmt(k, v)}` : null;
})
.filter(Boolean);
insertAuditTrail({
jobid: timeticket.job?.id ?? timeticket.jobid,
operation: AuditTrailMapping.timeticketupdated(
[original.employee.first_name, original.employee.last_name].filter(Boolean).join(" "),
original.date ? dayjs(original.date).format("YYYY-MM-DD") : "<<empty>>",
changed.length ? changed.join("; ") : "No changes"
)
});
// Refresh parent screens (Job Labor tab, etc.)
if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch();

View File

@@ -8,13 +8,14 @@ import { createStructuredSelector } from "reselect";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import JobsCreateComponent from "./jobs-create.component";
import JobCreateContext from "./jobs-create.context";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils";
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -22,10 +23,11 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser }) {
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser, insertAuditTrail }) {
const { t } = useTranslation();
const notification = useNotification();
@@ -84,6 +86,11 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr
newJobId: resp.data.insert_jobs.returning[0].id
});
logImEXEvent("manual_job_create_completed", {});
insertAuditTrail({
jobid: resp.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobmanualcreate(),
type: "jobmanualcreate"
});
setIsSubmitting(false);
})
.catch((error) => {

View File

@@ -120,8 +120,9 @@
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"assignedlinehours": "Assigned job lines totaling {{hours}} units to {{team}}.",
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
"billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
"billupdated": "Bill with invoice number {{invoice_number}} updated with the following details: {{values}}.",
"failedpayment": "Failed payment attempt.",
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
@@ -136,6 +137,9 @@
"jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.",
"jobinvoiced": "Job has been invoiced.",
"jobioucreated": "IOU Created.",
"joblineupdate": "Job line {{line_desc}} updated.",
"jobmanualcreate": "Job manually created.",
"jobmanuallineinsert": "Job line manually added with the following details: {{values}}.",
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
"jobnoteadded": "Note added to Job.",
"jobnotedeleted": "Note deleted from Job.",
@@ -151,7 +155,8 @@
"tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}",
"tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}",
"tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}",
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}",
"timeticketupdated": "Time Ticket for {{employee}} on {{date}} updated with the following details: {{details}}"
}
},
"billlines": {

View File

@@ -121,6 +121,7 @@
"assignedlinehours": "",
"billdeleted": "",
"billposted": "",
"billmarkforreexport": "",
"billupdated": "",
"failedpayment": "",
"jobassignmentchange": "",
@@ -136,6 +137,9 @@
"jobintake": "",
"jobinvoiced": "",
"jobioucreated": "",
"joblineupdate": "",
"jobmanualcreate": "",
"jobmanuallineinsert": "",
"jobmodifylbradj": "",
"jobnoteadded": "",
"jobnotedeleted": "",
@@ -151,7 +155,8 @@
"tasks_deleted": "",
"tasks_uncompleted": "",
"tasks_undeleted": "",
"tasks_updated": ""
"tasks_updated": "",
"timeticketupdated": ""
}
},
"billlines": {

View File

@@ -120,6 +120,7 @@
"appointmentinsert": "",
"assignedlinehours": "",
"billdeleted": "",
"billmarkforreexport": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",
@@ -136,6 +137,9 @@
"jobintake": "",
"jobinvoiced": "",
"jobioucreated": "",
"joblineupdate": "",
"jobmanualcreate": "",
"jobmanuallineinsert": "",
"jobmodifylbradj": "",
"jobnoteadded": "",
"jobnotedeleted": "",
@@ -151,7 +155,8 @@
"tasks_deleted": "",
"tasks_uncompleted": "",
"tasks_undeleted": "",
"tasks_updated": ""
"tasks_updated": "",
"timeticketupdated": ""
}
},
"billlines": {

View File

@@ -8,8 +8,9 @@ const AuditTrailMapping = {
appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
appointmentinsert: (start) => i18n.t("audit_trail.messages.appointmentinsert", { start }),
billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }),
billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }),
billupdated: (invoice_number, values) => i18n.t("audit_trail.messages.billupdated", { invoice_number, values }),
jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
jobchecklist: (type, inproduction, status) =>
@@ -25,6 +26,9 @@ const AuditTrailMapping = {
jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"),
joblineupdate: (line_desc) => i18n.t("audit_trail.messages.joblineupdate", { line_desc }),
jobmanualcreate: () => i18n.t("audit_trail.messages.jobmanualcreate"),
jobmanuallineinsert: (values) => i18n.t("audit_trail.messages.jobmanuallineinsert", { values }),
jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
@@ -71,7 +75,8 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.tasks_uncompleted", {
title,
uncompletedBy
})
}),
timeticketupdated: (employee, date, details) => i18n.t("audit_trail.messages.timeticketupdated", { employee, date, details })
};
export default AuditTrailMapping;

View File

@@ -24,6 +24,15 @@
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
comment: Project Mexico
- name: Chatter API Data Pump
webhook: '{{HASURA_API_URL}}/data/chatter-api'
schedule: 45 4 * * *
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
comment: ""
- name: Chatter Data Pump
webhook: '{{HASURA_API_URL}}/data/chatter'
schedule: 45 5 * * *

View File

@@ -51,7 +51,8 @@ awslocal ses verify-email-identity --email-address noreply@imex.online --region
# Secrets
ensure_secret_file "CHATTER_PRIVATE_KEY" "/tmp/certs/io-ftp-test.key"
ensure_secret_string "CHATTER_COMPANY_KEY_6713" "${CHATTER_COMPANY_KEY_6713:-REPLACE_ME}"
ensure_secret_string "CHATTER_COMPANY_KEY_6713" "${CHATTER_COMPANY_KEY_6713}"
ensure_secret_string "CHATTER_COMPANY_KEY_6746" "${CHATTER_COMPANY_KEY_6746}"
# Logs
ensure_log_group "development"

View File

@@ -787,7 +787,8 @@ async function RepairOrderChange(socket) {
// "DatePromisedUTC": "0001-01-01T00:00:00.0000000Z",
DateVehicleCompleted: socket.JobData.actual_completion,
// "DateCustomerNotified": "0001-01-01T00:00:00.0000000Z",
// "CSR": "String",
"CSR": "IMEX", //Hardcoded for now as the only shop that uses this RO posting is RC and they paid for a custom build.
Shop: "RB",
// "CSRRef": "00000000000000000000000000000000",
// "BookingUser": "String",
// "BookingUserRef": "00000000000000000000000000000000",

View File

@@ -33,8 +33,6 @@ const createLocation = async (req, res) => {
const { logger } = req;
const { bodyshopID, googlePlaceID } = req.body;
console.dir({ body: req.body });
if (!DEFAULT_COMPANY_ID) {
logger.log("chatter-create-location-no-default-company", "warn", null, null, { bodyshopID });
return res.json({ success: false, message: "No default company set" });
@@ -67,7 +65,7 @@ const createLocation = async (req, res) => {
const chatterApi = await createChatterClient(DEFAULT_COMPANY_ID);
const locationIdentifier = `${DEFAULT_COMPANY_ID}-${bodyshop.id}`;
const locationIdentifier = bodyshop?.imexshopid ?? `${DEFAULT_COMPANY_ID}-${bodyshop.id}`;
const locationPayload = {
name: bodyshop.shopname,

View File

@@ -334,30 +334,48 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome
socket.emit("export-success", JobData.id);
} else {
//There was something wrong. Throw an error to trigger clean up.
//throw new Error("Error posting DMS Batch Transaction");
const batchPostError = new Error(DmsBatchTxnPost.sendline || "Error posting DMS Batch Transaction");
batchPostError.errorData = { DMSTransHeader, DmsBatchTxnPost };
throw batchPostError;
}
} catch {
} catch (error) {
//Clean up the transaction and insert a faild error code
// //Get the error code
CreateFortellisLogEvent(socket, "DEBUG", `{6.1} Getting errors for Transaction ID ${DMSTransHeader.transID}`);
const DmsError = await QueryDmsErrWip({ socket, redisHelpers, JobData });
// //Delete the transaction
let dmsErrors = [];
try {
const DmsError = await QueryDmsErrWip({ socket, redisHelpers, JobData });
dmsErrors = Array.isArray(DmsError?.errMsg) ? DmsError.errMsg.filter((e) => e !== null && e !== "") : [];
dmsErrors.forEach((e) => {
CreateFortellisLogEvent(socket, "ERROR", `Error encountered in posting transaction => ${e} `);
});
} catch (queryError) {
CreateFortellisLogEvent(
socket,
"ERROR",
`{6.1} Unable to read ErrWIP for Transaction ID ${DMSTransHeader.transID}: ${queryError.message}`
);
}
//Delete the transaction, even if querying ErrWIP fails.
CreateFortellisLogEvent(socket, "DEBUG", `{6.2} Deleting Transaction ID ${DMSTransHeader.transID}`);
try {
await DeleteDmsWip({ socket, redisHelpers, JobData });
} catch (cleanupError) {
CreateFortellisLogEvent(
socket,
"ERROR",
`{6.2} Failed cleanup for Transaction ID ${DMSTransHeader.transID}: ${cleanupError.message}`
);
}
await DeleteDmsWip({ socket, redisHelpers, JobData });
DmsError.errMsg.map(
(e) =>
e !== null &&
e !== "" &&
CreateFortellisLogEvent(socket, "ERROR", `Error encountered in posting transaction => ${e} `)
);
await InsertFailedExportLog({
socket,
JobData,
error: DmsError.errMsg
});
if (!error.errorData || typeof error.errorData !== "object") {
error.errorData = {};
}
error.errorData.issues = dmsErrors.length ? dmsErrors : [error.message];
throw error;
}
}
} catch (error) {

View File

@@ -116,6 +116,32 @@ async function TotalsServerSide(req, res) {
ret.totals.ttl_tax_adjustment = Dinero({ amount: Math.round(ttlTaxDifference * 100) });
ret.totals.total_repairs = ret.totals.total_repairs.add(ret.totals.ttl_tax_adjustment);
ret.totals.net_repairs = ret.totals.net_repairs.add(ret.totals.ttl_tax_adjustment);
if (Math.abs(totalUsTaxes) === 0) {
const laborRates = Object.values(job.cieca_pfl)
.map((v) => v.lbr_taxp)
.filter((v) => v != null);
const materialRates = Object.values(job.materials)
.map((v) => v.mat_taxp)
.filter((v) => v != null);
const partsRates = Object.values(job.parts_tax_rates)
.map((v) => {
const field = v.prt_tax_rt ?? v.part_tax_rt;
if (field == null) return null;
const raw = typeof field === "object" ? field.parsedValue : field;
return raw != null ? raw * 100 : null;
})
.filter((v) => v != null);
const taxRate = Math.max(...laborRates, ...materialRates, ...partsRates);
const totalTaxes = ret.totals.taxableAmounts.total.multiply(taxRate > 1 ? taxRate / 100 : taxRate);
ret.totals.taxableAmounts.total = ret.totals.taxableAmounts.total
.multiply(emsTaxTotal)
.divide(totalTaxes.toUnit());
} else {
ret.totals.taxableAmounts.total = ret.totals.taxableAmounts.total.multiply(emsTaxTotal).divide(totalUsTaxes);
}
logger.log("job-totals-USA-ttl-tax-adj", "debug", null, job.id, {
adjAmount: ttlTaxDifference
});
@@ -993,6 +1019,8 @@ function CalculateTaxesTotals(job, otherTotals) {
}
});
taxableAmounts.total = Object.values(taxableAmounts).reduce((acc, amount) => acc.add(amount), Dinero({ amount: 0 }));
// console.log("*** Taxable Amounts***");
// console.table(JSON.parse(JSON.stringify(taxableAmounts)));
@@ -1203,6 +1231,7 @@ function CalculateTaxesTotals(job, otherTotals) {
let ret = {
subtotal: subtotal,
taxableAmounts: taxableAmounts,
federal_tax: subtotal
.percentage((job.federal_tax_rate || 0) * 100)
.add(otherTotals.additional.pvrt.percentage((job.federal_tax_rate || 0) * 100)),