Compare commits
22 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bda497d8c | ||
|
|
a4dbc5250e | ||
|
|
a1d0e2df93 | ||
|
|
9a86a337bb | ||
|
|
7688f22161 | ||
|
|
efdcd06921 | ||
|
|
c0a37d7c1a | ||
|
|
6759bc5865 | ||
|
|
04732fc6cd | ||
|
|
a65a34ef1f | ||
|
|
1ea7798eeb | ||
|
|
7739d48741 | ||
|
|
074be66b8c | ||
|
|
8db8744782 | ||
|
|
c2d8d78e0a | ||
|
|
71aec6d0c5 | ||
|
|
f89d7865fa | ||
|
|
8fd368ebb4 | ||
|
|
132fc0a20f | ||
|
|
9ea2d83043 | ||
|
|
abad7d5f00 | ||
|
|
c97213bc96 |
@@ -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,
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -64,7 +64,7 @@ function normalizeJobAllocations(ack) {
|
||||
* RR-specific DMS Allocations Summary
|
||||
* Focused on what we actually send to RR:
|
||||
* - ROGOG (split by taxable / non-taxable segments)
|
||||
* - ROLABOR shell
|
||||
* - ROLABOR labor rows with bill hours / rates
|
||||
*
|
||||
* The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags)
|
||||
* is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog.
|
||||
@@ -181,21 +181,30 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
|
||||
const rolaborRows = useMemo(() => {
|
||||
if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return [];
|
||||
|
||||
return rolaborPreview.ops.map((op, idx) => {
|
||||
const rowOpCode = opCode || op.opCode;
|
||||
return rolaborPreview.ops
|
||||
.filter((op) =>
|
||||
[op.bill?.jobTotalHrs, op.bill?.billTime, op.bill?.billRate, op.amount?.custPrice, op.amount?.totalAmt]
|
||||
.map((value) => Number.parseFloat(value ?? "0"))
|
||||
.some((value) => !Number.isNaN(value) && value !== 0)
|
||||
)
|
||||
.map((op, idx) => {
|
||||
const rowOpCode = opCode || op.opCode;
|
||||
|
||||
return {
|
||||
key: `${op.jobNo}-${idx}`,
|
||||
opCode: rowOpCode,
|
||||
jobNo: op.jobNo,
|
||||
custPayTypeFlag: op.custPayTypeFlag,
|
||||
custTxblNtxblFlag: op.custTxblNtxblFlag,
|
||||
payType: op.bill?.payType,
|
||||
amtType: op.amount?.amtType,
|
||||
custPrice: op.amount?.custPrice,
|
||||
totalAmt: op.amount?.totalAmt
|
||||
};
|
||||
});
|
||||
return {
|
||||
key: `${op.jobNo}-${idx}`,
|
||||
opCode: rowOpCode,
|
||||
jobNo: op.jobNo,
|
||||
custPayTypeFlag: op.custPayTypeFlag,
|
||||
custTxblNtxblFlag: op.custTxblNtxblFlag,
|
||||
payType: op.bill?.payType,
|
||||
jobTotalHrs: op.bill?.jobTotalHrs,
|
||||
billTime: op.bill?.billTime,
|
||||
billRate: op.bill?.billRate,
|
||||
amtType: op.amount?.amtType,
|
||||
custPrice: op.amount?.custPrice,
|
||||
totalAmt: op.amount?.totalAmt
|
||||
};
|
||||
});
|
||||
}, [rolaborPreview, opCode]);
|
||||
|
||||
// Totals for ROGOG (sum custPrice + dlrCost over all lines)
|
||||
@@ -245,6 +254,9 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
|
||||
{ title: "CustPayType", dataIndex: "custPayTypeFlag", key: "custPayTypeFlag" },
|
||||
{ title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" },
|
||||
{ title: "PayType", dataIndex: "payType", key: "payType" },
|
||||
{ title: "JobTotalHrs", dataIndex: "jobTotalHrs", key: "jobTotalHrs" },
|
||||
{ title: "BillTime", dataIndex: "billTime", key: "billTime" },
|
||||
{ title: "BillRate", dataIndex: "billRate", key: "billRate" },
|
||||
{ title: "AmtType", dataIndex: "amtType", key: "amtType" },
|
||||
{ title: "CustPrice", dataIndex: "custPrice", key: "custPrice" },
|
||||
{ title: "TotalAmt", dataIndex: "totalAmt", key: "totalAmt" }
|
||||
@@ -317,12 +329,13 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
|
||||
children: (
|
||||
<>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||||
This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG.
|
||||
This mirrors the labor rows RR will receive, including weighted bill hours and rates derived from the
|
||||
job's labor lines.
|
||||
</Typography.Paragraph>
|
||||
<ResponsiveTable
|
||||
pagination={false}
|
||||
columns={rolaborColumns}
|
||||
mobileColumnKeys={["jobNo", "opCode", "breakOut", "itemType"]}
|
||||
mobileColumnKeys={["jobNo", "opCode", "billRate", "custPrice"]}
|
||||
rowKey="key"
|
||||
dataSource={rolaborRows}
|
||||
locale={{ emptyText: "No ROLABOR lines would be generated." }}
|
||||
|
||||
@@ -58,6 +58,7 @@ export function ProductionColumnsComponent({
|
||||
|
||||
const columnKeys = columns.map((i) => i.key);
|
||||
const cols = dataSource({
|
||||
bodyshop,
|
||||
technician,
|
||||
data,
|
||||
state: tableState,
|
||||
|
||||
@@ -609,7 +609,19 @@ const productionListColumnsData = ({ technician, state, activeStatuses, data, bo
|
||||
ellipsis: true,
|
||||
|
||||
render: (text, record) => <TimeFormatter>{record.date_repairstarted}</TimeFormatter>
|
||||
}
|
||||
},
|
||||
...(bodyshop && bodyshop.rr_dealerid
|
||||
? [
|
||||
{
|
||||
title: i18n.t("jobs.fields.dms.id"),
|
||||
dataIndex: "dms_id",
|
||||
key: "dms_id",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.dms_id, b.dms_id),
|
||||
sortOrder: state.sortedInfo.columnKey === "dms_id" && state.sortedInfo.order
|
||||
}
|
||||
]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
export default productionListColumnsData;
|
||||
|
||||
@@ -244,6 +244,7 @@ export function ProductionListConfigManager({
|
||||
nextConfig.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
state: ensureDefaultState(state),
|
||||
refetch,
|
||||
@@ -270,6 +271,7 @@ export function ProductionListConfigManager({
|
||||
activeConfig.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
state: ensureDefaultState(state),
|
||||
refetch,
|
||||
|
||||
@@ -197,6 +197,7 @@ export const QUERY_EXACT_JOB_IN_PRODUCTION = gql`
|
||||
employee_prep
|
||||
employee_csr
|
||||
date_repairstarted
|
||||
dms_id
|
||||
joblines_status {
|
||||
part_type
|
||||
status
|
||||
@@ -269,6 +270,7 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql`
|
||||
employee_prep
|
||||
employee_csr
|
||||
date_repairstarted
|
||||
dms_id
|
||||
joblines_status {
|
||||
part_type
|
||||
status
|
||||
@@ -2671,6 +2673,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
suspended
|
||||
job_totals
|
||||
date_repairstarted
|
||||
dms_id
|
||||
joblines_status {
|
||||
part_type
|
||||
status
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"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.",
|
||||
"failedpayment": "Failed payment attempt.",
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
"assignedlinehours": "",
|
||||
"billdeleted": "",
|
||||
"billposted": "",
|
||||
"billmarkforreexport": "",
|
||||
"billupdated": "",
|
||||
"failedpayment": "",
|
||||
"jobassignmentchange": "",
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"appointmentinsert": "",
|
||||
"assignedlinehours": "",
|
||||
"billdeleted": "",
|
||||
"billmarkforreexport": "",
|
||||
"billposted": "",
|
||||
"billupdated": "",
|
||||
"failedpayment": "",
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 }),
|
||||
jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
|
||||
|
||||
@@ -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 * * *
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr");
|
||||
|
||||
const CHATTER_BASE_URL = process.env.CHATTER_API_BASE_URL || "https://api.chatterresearch.com";
|
||||
const AWS_REGION = process.env.AWS_REGION || "ca-central-1";
|
||||
|
||||
// Configure SecretsManager client with localstack support
|
||||
const secretsClientOptions = {
|
||||
region: AWS_REGION,
|
||||
region: InstanceRegion(),
|
||||
credentials: defaultProvider()
|
||||
};
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
if (isLocal) {
|
||||
secretsClientOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
secretsClientOptions.endpoint = InstanceLocalStackEndpoint();
|
||||
}
|
||||
|
||||
const secretsClient = new SecretsManagerClient(secretsClientOptions);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,7 @@ const Dinero = require("dinero.js");
|
||||
const moment = require("moment-timezone");
|
||||
const logger = require("../utils/logger");
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceIsLocalStackEnabled } = require("../utils/instanceMgr");
|
||||
const fs = require("fs");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail");
|
||||
@@ -35,10 +35,9 @@ const S3_BUCKET_NAME = InstanceManager({
|
||||
rome: "rome-carfax-uploads"
|
||||
});
|
||||
const region = InstanceManager.InstanceRegion;
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
const uploadToS3 = (jsonObj, bucketName = S3_BUCKET_NAME) => {
|
||||
const webPath = isLocal
|
||||
const webPath = InstanceIsLocalStackEnabled()
|
||||
? `https://${bucketName}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
|
||||
: `https://${bucketName}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const logger = require("../utils/logger");
|
||||
const fs = require("fs");
|
||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr");
|
||||
|
||||
let Client = require("ssh2-sftp-client");
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
@@ -151,10 +152,8 @@ async function getPrivateKey() {
|
||||
credentials: defaultProvider()
|
||||
};
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
if (isLocal) {
|
||||
secretsClientOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
secretsClientOptions.endpoint = InstanceLocalStackEndpoint();
|
||||
}
|
||||
|
||||
const client = new SecretsManagerClient(secretsClientOptions);
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { InstanceRegion } = require("../utils/instanceMgr");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("../utils/instanceMgr");
|
||||
const aws = require("@aws-sdk/client-ses");
|
||||
const nodemailer = require("nodemailer");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
const sesConfig = {
|
||||
apiVersion: "latest",
|
||||
credentials: defaultProvider(),
|
||||
region: InstanceRegion()
|
||||
};
|
||||
|
||||
if (isLocal) {
|
||||
sesConfig.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
sesConfig.endpoint = InstanceLocalStackEndpoint();
|
||||
logger.logger.debug(`SES Mailer set to LocalStack end point: ${sesConfig.endpoint}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -46,6 +46,11 @@ const summarizeAllocationsArray = (arr) =>
|
||||
cost: summarizeMoney(a.cost)
|
||||
}));
|
||||
|
||||
const toFiniteNumber = (value) => {
|
||||
const parsed = Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal per-center bucket shape for *sales*.
|
||||
* We keep separate buckets for RR so we can split
|
||||
@@ -62,6 +67,8 @@ function emptyCenterBucket() {
|
||||
// Labor
|
||||
laborTaxableSale: zero, // labor that should be taxed in RR
|
||||
laborNonTaxableSale: zero, // labor that should NOT be taxed in RR
|
||||
laborTaxableHours: 0,
|
||||
laborNonTaxableHours: 0,
|
||||
|
||||
// Extras (MAPA/MASH/towing/PAO/etc)
|
||||
extrasSale: zero, // total extras (taxable + non-taxable)
|
||||
@@ -453,6 +460,7 @@ function buildProfitCenterHash(job, debugLog, taxContext) {
|
||||
|
||||
const rateKey = `rate_${val.mod_lbr_ty.toLowerCase()}`;
|
||||
const rate = job[rateKey];
|
||||
const lineHours = toFiniteNumber(val.mod_lb_hrs);
|
||||
|
||||
const laborAmount = Dinero({
|
||||
amount: Math.round(rate * 100)
|
||||
@@ -460,8 +468,10 @@ function buildProfitCenterHash(job, debugLog, taxContext) {
|
||||
|
||||
if (isLaborTaxable(val, taxContext)) {
|
||||
bucket.laborTaxableSale = bucket.laborTaxableSale.add(laborAmount);
|
||||
bucket.laborTaxableHours += lineHours;
|
||||
} else {
|
||||
bucket.laborNonTaxableSale = bucket.laborNonTaxableSale.add(laborAmount);
|
||||
bucket.laborNonTaxableHours += lineHours;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +488,8 @@ function buildProfitCenterHash(job, debugLog, taxContext) {
|
||||
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
|
||||
laborTaxable: summarizeMoney(b.laborTaxableSale),
|
||||
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
|
||||
laborTaxableHours: b.laborTaxableHours,
|
||||
laborNonTaxableHours: b.laborNonTaxableHours,
|
||||
extras: summarizeMoney(b.extrasSale),
|
||||
extrasTaxable: summarizeMoney(b.extrasTaxableSale),
|
||||
extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
|
||||
@@ -916,6 +928,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
|
||||
// Labor
|
||||
laborTaxableSale: bucket.laborTaxableSale,
|
||||
laborNonTaxableSale: bucket.laborNonTaxableSale,
|
||||
laborTaxableHours: bucket.laborTaxableHours,
|
||||
laborNonTaxableHours: bucket.laborNonTaxableHours,
|
||||
|
||||
// Extras
|
||||
extrasSale,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
||||
const { buildRRRepairOrderPayload, buildMinimalRolaborFromJob } = require("./rr-job-helpers");
|
||||
const { buildClientAndOpts } = require("./rr-lookup");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { withRRRequestXml } = require("./rr-log-xml");
|
||||
@@ -56,6 +56,27 @@ const deriveRRStatus = (rrRes = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const resolveRROpCode = (bodyshop, txEnvelope = {}) => {
|
||||
const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
|
||||
let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
|
||||
|
||||
if (!opCodeOverride) {
|
||||
const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
|
||||
const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
|
||||
const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
|
||||
|
||||
if (opPrefix || opBase || opSuffix) {
|
||||
const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
|
||||
if (combined) {
|
||||
opCodeOverride = combined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!opCodeOverride && !resolvedBaseOpCode) return null;
|
||||
return String(opCodeOverride || resolvedBaseOpCode).trim() || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Early RO Creation: Create a minimal RR Repair Order with basic info (customer, advisor, mileage, story).
|
||||
* Used when creating RO from convert button or admin page before full job export.
|
||||
@@ -93,7 +114,9 @@ const createMinimalRRRepairOrder = async (args) => {
|
||||
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
|
||||
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
|
||||
|
||||
// Build minimal RO payload - just header, no allocations/parts/labor
|
||||
// Build minimal RO payload for early review mode.
|
||||
// We keep it lightweight, but include a single labor row when we can so Ignite
|
||||
// exposes the labor subsection for editing.
|
||||
const cleanVin =
|
||||
(job?.v_vin || "")
|
||||
.toString()
|
||||
@@ -116,6 +139,12 @@ const createMinimalRRRepairOrder = async (args) => {
|
||||
resolvedMileageIn: mileageIn
|
||||
});
|
||||
|
||||
const earlyRoOpCode = resolveRROpCode(bodyshop, txEnvelope);
|
||||
const earlyRoLabor = buildMinimalRolaborFromJob(job, {
|
||||
opCode: earlyRoOpCode,
|
||||
payType: "Cust"
|
||||
});
|
||||
|
||||
const payload = {
|
||||
customerNo: String(selected),
|
||||
advisorNo: String(advisorNo),
|
||||
@@ -141,9 +170,14 @@ const createMinimalRRRepairOrder = async (args) => {
|
||||
if (makeOverride) {
|
||||
payload.makeOverride = makeOverride;
|
||||
}
|
||||
if (earlyRoLabor) {
|
||||
payload.rolabor = earlyRoLabor;
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "Creating minimal RR Repair Order (early creation)", {
|
||||
payload
|
||||
payload,
|
||||
earlyRoOpCode,
|
||||
hasRolabor: !!earlyRoLabor
|
||||
});
|
||||
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
@@ -221,15 +255,10 @@ const updateRRRepairOrderWithFullData = async (args) => {
|
||||
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
|
||||
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
|
||||
|
||||
// Optional RR OpCode segments coming from the FE (RRPostForm)
|
||||
const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
|
||||
const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
|
||||
const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
|
||||
|
||||
// RR-only extras
|
||||
let rrCentersConfig = null;
|
||||
let allocations = null;
|
||||
let opCode = null;
|
||||
const opCode = resolveRROpCode(bodyshop, txEnvelope);
|
||||
|
||||
// 1) Responsibility center config (for visibility / debugging)
|
||||
try {
|
||||
@@ -280,28 +309,9 @@ const updateRRRepairOrderWithFullData = async (args) => {
|
||||
allocations = [];
|
||||
}
|
||||
|
||||
const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
|
||||
|
||||
let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
|
||||
|
||||
// If the FE only sends segments, combine them here.
|
||||
if (!opCodeOverride && (opPrefix || opBase || opSuffix)) {
|
||||
const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
|
||||
if (combined) {
|
||||
opCodeOverride = combined;
|
||||
}
|
||||
}
|
||||
|
||||
if (opCodeOverride || resolvedBaseOpCode) {
|
||||
opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null;
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
|
||||
opCode,
|
||||
baseFromConfig: resolvedBaseOpCode,
|
||||
opPrefix,
|
||||
opBase,
|
||||
opSuffix
|
||||
baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop)
|
||||
});
|
||||
|
||||
// Build full RO payload for update with allocations
|
||||
@@ -426,15 +436,10 @@ const exportJobToRR = async (args) => {
|
||||
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
|
||||
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
|
||||
|
||||
// Optional RR OpCode segments coming from the FE (RRPostForm)
|
||||
const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
|
||||
const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
|
||||
const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
|
||||
|
||||
// RR-only extras
|
||||
let rrCentersConfig = null;
|
||||
let allocations = null;
|
||||
let opCode = null;
|
||||
const opCode = resolveRROpCode(bodyshop, txEnvelope);
|
||||
|
||||
// 1) Responsibility center config (for visibility / debugging)
|
||||
try {
|
||||
@@ -477,28 +482,9 @@ const exportJobToRR = async (args) => {
|
||||
allocations = [];
|
||||
}
|
||||
|
||||
const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
|
||||
|
||||
let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
|
||||
|
||||
// If the FE only sends segments, combine them here.
|
||||
if (!opCodeOverride && (opPrefix || opBase || opSuffix)) {
|
||||
const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
|
||||
if (combined) {
|
||||
opCodeOverride = combined;
|
||||
}
|
||||
}
|
||||
|
||||
if (opCodeOverride || resolvedBaseOpCode) {
|
||||
opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null;
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
|
||||
opCode,
|
||||
baseFromConfig: resolvedBaseOpCode,
|
||||
opPrefix,
|
||||
opBase,
|
||||
opSuffix
|
||||
baseFromConfig: resolveRROpCodeFromBodyshop(bodyshop)
|
||||
});
|
||||
|
||||
// Build RO payload for create.
|
||||
|
||||
@@ -52,6 +52,19 @@ const asN2 = (dineroLike) => {
|
||||
return amount.toFixed(2);
|
||||
};
|
||||
|
||||
const toFiniteNumber = (value) => {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number.parseFloat(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize various "money-like" shapes to integer cents.
|
||||
* Supports:
|
||||
@@ -100,6 +113,100 @@ const toMoneyCents = (value) => {
|
||||
|
||||
const asN2FromCents = (cents) => asN2({ amount: Number.isFinite(cents) ? cents : 0, precision: 2 });
|
||||
|
||||
const formatDecimal = (value, maxDecimals = 2) => {
|
||||
const factor = Math.pow(10, maxDecimals);
|
||||
const rounded = Math.round(Math.max(0, toFiniteNumber(value)) * factor) / factor;
|
||||
if (!Number.isFinite(rounded)) return "0";
|
||||
return rounded.toFixed(maxDecimals).replace(/\.?0+$/, "") || "0";
|
||||
};
|
||||
|
||||
const buildRolaborBillFields = ({ amountUnits = 0, hours = 0, rate = 0 } = {}) => {
|
||||
const normalizedAmount = toFiniteNumber(amountUnits);
|
||||
|
||||
if (normalizedAmount <= 0) {
|
||||
return {
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
};
|
||||
}
|
||||
|
||||
let resolvedHours = toFiniteNumber(hours);
|
||||
let resolvedRate = toFiniteNumber(rate);
|
||||
|
||||
if (resolvedHours > 0 && resolvedRate <= 0) {
|
||||
resolvedRate = normalizedAmount / resolvedHours;
|
||||
} else if (resolvedRate > 0 && resolvedHours <= 0) {
|
||||
resolvedHours = normalizedAmount / resolvedRate;
|
||||
} else if (resolvedHours <= 0 && resolvedRate <= 0) {
|
||||
// Keep the math internally consistent even if the source job has dollars but no usable hours.
|
||||
resolvedHours = 1;
|
||||
resolvedRate = normalizedAmount;
|
||||
}
|
||||
|
||||
return {
|
||||
jobTotalHrs: formatDecimal(resolvedHours),
|
||||
billTime: formatDecimal(resolvedHours),
|
||||
billRate: resolvedRate.toFixed(2)
|
||||
};
|
||||
};
|
||||
|
||||
const buildMinimalRolaborFromJob = (job, { opCode, payType = "Cust" } = {}) => {
|
||||
const trimmedOpCode = opCode != null ? String(opCode).trim() : "";
|
||||
if (!job || !trimmedOpCode) return null;
|
||||
|
||||
let totalHours = 0;
|
||||
let totalAmountUnits = 0;
|
||||
|
||||
for (const line of job?.joblines || []) {
|
||||
const laborType = typeof line?.mod_lbr_ty === "string" ? line.mod_lbr_ty.trim() : "";
|
||||
if (!laborType) continue;
|
||||
|
||||
const lineHours = toFiniteNumber(line?.mod_lb_hrs ?? line?.db_hrs);
|
||||
const configuredRate = toFiniteNumber(job?.[`rate_${laborType.toLowerCase()}`]);
|
||||
let lineAmountUnits = toFiniteNumber(line?.lbr_amt);
|
||||
|
||||
if (lineAmountUnits <= 0 && lineHours > 0 && configuredRate > 0) {
|
||||
lineAmountUnits = lineHours * configuredRate;
|
||||
}
|
||||
|
||||
if (lineAmountUnits <= 0 && lineHours <= 0) continue;
|
||||
|
||||
totalHours += lineHours;
|
||||
totalAmountUnits += lineAmountUnits;
|
||||
}
|
||||
|
||||
if (totalAmountUnits <= 0 && totalHours <= 0) return null;
|
||||
|
||||
const bill = buildRolaborBillFields({
|
||||
amountUnits: totalAmountUnits,
|
||||
hours: totalHours,
|
||||
rate: totalHours > 0 ? totalAmountUnits / totalHours : 0
|
||||
});
|
||||
const formattedAmount = totalAmountUnits.toFixed(2);
|
||||
|
||||
return {
|
||||
ops: [
|
||||
{
|
||||
opCode: trimmedOpCode,
|
||||
jobNo: "1",
|
||||
custPayTypeFlag: "C",
|
||||
custTxblNtxblFlag: toFiniteNumber(job?.tax_lbr_rt) > 0 ? "T" : "N",
|
||||
bill: {
|
||||
payType,
|
||||
...bill
|
||||
},
|
||||
amount: {
|
||||
payType,
|
||||
amtType: "Job",
|
||||
custPrice: formattedAmount,
|
||||
totalAmt: formattedAmount
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build RR estimate block from allocation totals.
|
||||
* @param {Array} allocations
|
||||
@@ -326,6 +433,13 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
|
||||
// Each segment becomes its own op / JobNo with a single line
|
||||
segments.forEach((seg, idx) => {
|
||||
const jobNo = String(ops.length + 1); // global, 1-based JobNo across all centers/segments
|
||||
const isLaborSegment = seg.kind === "laborTaxable" || seg.kind === "laborNonTaxable";
|
||||
const segmentHours = isLaborSegment
|
||||
? seg.kind === "laborTaxable"
|
||||
? toFiniteNumber(alloc.laborTaxableHours)
|
||||
: toFiniteNumber(alloc.laborNonTaxableHours)
|
||||
: 0;
|
||||
const segmentBillRate = isLaborSegment && segmentHours > 0 ? seg.saleCents / 100 / segmentHours : 0;
|
||||
|
||||
const line = {
|
||||
breakOut,
|
||||
@@ -349,7 +463,9 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
|
||||
// Extra metadata for UI / debugging
|
||||
segmentKind: seg.kind,
|
||||
segmentIndex: idx,
|
||||
segmentCount
|
||||
segmentCount,
|
||||
segmentHours,
|
||||
segmentBillRate
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -368,9 +484,9 @@ const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo
|
||||
*
|
||||
* We still keep a 1:1 mapping with GOG ops: each op gets a corresponding
|
||||
* OpCodeLaborInfo entry using the same JobNo and the same tax flag as its
|
||||
* GOG line. Labor-specific hours/rate remain zeroed out, but actual labor
|
||||
* sale amounts are mirrored into ROLABOR for labor segments so RR receives
|
||||
* the expected labor pricing on updates. Non-labor ops remain zeroed.
|
||||
* GOG line. Labor sale amounts are mirrored into ROLABOR and, when hours
|
||||
* are available from allocations, weighted bill hours/rates are also
|
||||
* populated so the labor subsection is editable in Ignite.
|
||||
*
|
||||
* @param {Object} rogg - result of buildRogogFromAllocations
|
||||
* @param {Object} opts
|
||||
@@ -391,6 +507,17 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
|
||||
const linePayType = firstLine.custPayTypeFlag || "C";
|
||||
const isLaborSegment = op.segmentKind === "laborTaxable" || op.segmentKind === "laborNonTaxable";
|
||||
const laborAmount = isLaborSegment ? String(firstLine?.amount?.custPrice ?? "0") : "0";
|
||||
const laborBill = isLaborSegment
|
||||
? buildRolaborBillFields({
|
||||
amountUnits: laborAmount,
|
||||
hours: op.segmentHours,
|
||||
rate: op.segmentBillRate
|
||||
})
|
||||
: {
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
};
|
||||
|
||||
return {
|
||||
opCode: op.opCode,
|
||||
@@ -399,9 +526,7 @@ const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
|
||||
custTxblNtxblFlag: txFlag,
|
||||
bill: {
|
||||
payType,
|
||||
jobTotalHrs: "0",
|
||||
billTime: "0",
|
||||
billRate: "0"
|
||||
...laborBill
|
||||
},
|
||||
amount: {
|
||||
payType,
|
||||
@@ -686,5 +811,6 @@ module.exports = {
|
||||
normalizeCustomerCandidates,
|
||||
normalizeVehicleCandidates,
|
||||
buildRogogFromAllocations,
|
||||
buildRolaborFromRogog
|
||||
buildRolaborFromRogog,
|
||||
buildMinimalRolaborFromJob
|
||||
};
|
||||
|
||||
118
server/rr/rr-job-helpers.test.js
Normal file
118
server/rr/rr-job-helpers.test.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const mock = require("mock-require");
|
||||
|
||||
const graphClientModuleId = require.resolve("../graphql-client/graphql-client");
|
||||
const queriesModuleId = require.resolve("../graphql-client/queries");
|
||||
const helpersModuleId = require.resolve("./rr-job-helpers");
|
||||
|
||||
const loadHelpers = () => {
|
||||
mock.stopAll();
|
||||
mock(graphClientModuleId, { client: { request: async () => ({}) } });
|
||||
mock(queriesModuleId, { GET_JOB_BY_PK: "GET_JOB_BY_PK" });
|
||||
delete require.cache[helpersModuleId];
|
||||
return require(helpersModuleId);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
mock.stopAll();
|
||||
delete require.cache[helpersModuleId];
|
||||
});
|
||||
|
||||
describe("server/rr/rr-job-helpers", () => {
|
||||
it("builds a single early-RO labor row from aggregated job labor", () => {
|
||||
const { buildMinimalRolaborFromJob } = loadHelpers();
|
||||
|
||||
const rolabor = buildMinimalRolaborFromJob(
|
||||
{
|
||||
tax_lbr_rt: 13,
|
||||
joblines: [
|
||||
{ mod_lbr_ty: "LAB", mod_lb_hrs: 2, lbr_amt: 200 },
|
||||
{ mod_lbr_ty: "LAD", mod_lb_hrs: 1.5, lbr_amt: 180 }
|
||||
]
|
||||
},
|
||||
{ opCode: "51DOZ" }
|
||||
);
|
||||
|
||||
expect(rolabor).toEqual({
|
||||
ops: [
|
||||
{
|
||||
opCode: "51DOZ",
|
||||
jobNo: "1",
|
||||
custPayTypeFlag: "C",
|
||||
custTxblNtxblFlag: "T",
|
||||
bill: {
|
||||
payType: "Cust",
|
||||
jobTotalHrs: "3.5",
|
||||
billTime: "3.5",
|
||||
billRate: "108.57"
|
||||
},
|
||||
amount: {
|
||||
payType: "Cust",
|
||||
amtType: "Job",
|
||||
custPrice: "380.00",
|
||||
totalAmt: "380.00"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it("populates labor bill fields from allocation hours on the full RR payload", () => {
|
||||
const { buildRRRepairOrderPayload } = loadHelpers();
|
||||
|
||||
const payload = buildRRRepairOrderPayload({
|
||||
job: {
|
||||
id: "job-1",
|
||||
ro_number: "RO-123",
|
||||
v_vin: "1HGBH41JXMN109186"
|
||||
},
|
||||
selectedCustomer: { customerNo: "1134485" },
|
||||
advisorNo: "70754",
|
||||
allocations: [
|
||||
{
|
||||
center: "Body Labor",
|
||||
partsSale: { amount: 0, precision: 2 },
|
||||
laborTaxableSale: { amount: 24000, precision: 2 },
|
||||
laborNonTaxableSale: { amount: 0, precision: 2 },
|
||||
extrasSale: { amount: 0, precision: 2 },
|
||||
totalSale: { amount: 24000, precision: 2 },
|
||||
cost: { amount: 12000, precision: 2 },
|
||||
laborTaxableHours: 2,
|
||||
laborNonTaxableHours: 0,
|
||||
profitCenter: {
|
||||
rr_gogcode: "BL",
|
||||
rr_item_type: "G",
|
||||
accountdesc: "BODY LABOR"
|
||||
}
|
||||
}
|
||||
],
|
||||
opCode: "51DOZ"
|
||||
});
|
||||
|
||||
expect(payload.rolabor).toEqual({
|
||||
ops: [
|
||||
{
|
||||
opCode: "51DOZ",
|
||||
jobNo: "1",
|
||||
custPayTypeFlag: "C",
|
||||
custTxblNtxblFlag: "T",
|
||||
bill: {
|
||||
payType: "Cust",
|
||||
jobTotalHrs: "2",
|
||||
billTime: "2",
|
||||
billRate: "120.00"
|
||||
},
|
||||
amount: {
|
||||
payType: "Cust",
|
||||
amtType: "Job",
|
||||
custPrice: "240.00",
|
||||
totalAmt: "240.00"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,14 +7,24 @@
|
||||
* @property { string | object | function } promanager Return this prop if Rome.
|
||||
* @property { string | object | function } imex Return this prop if Rome.
|
||||
*/
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
|
||||
function InstanceManager({ args, instance, debug, executeFunction, rome, promanager, imex }) {
|
||||
/**
|
||||
* InstanceManager is a utility function that determines which property to return based on the current instance type.
|
||||
* @param param0
|
||||
* @param param0.args
|
||||
* @param param0.instance
|
||||
* @param param0.debug
|
||||
* @param param0.executeFunction
|
||||
* @param param0.rome
|
||||
* @param param0.promanager
|
||||
* @param param0.imex
|
||||
* @returns {*|null}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceManager = ({ args, instance, debug, executeFunction, rome, promanager, imex }) => {
|
||||
let propToReturn = null;
|
||||
|
||||
//TODO: Remove after debugging.
|
||||
if (promanager) {
|
||||
console.trace("ProManager Prop was used");
|
||||
}
|
||||
switch (instance || process.env.INSTANCE) {
|
||||
case "IMEX":
|
||||
propToReturn = imex;
|
||||
@@ -50,15 +60,42 @@ function InstanceManager({ args, instance, debug, executeFunction, rome, promana
|
||||
}
|
||||
if (executeFunction && typeof propToReturn === "function") return propToReturn(...args);
|
||||
return propToReturn === undefined ? null : propToReturn;
|
||||
}
|
||||
};
|
||||
|
||||
exports.InstanceRegion = () =>
|
||||
/**
|
||||
* Returns the AWS region to be used for the current instance, which is determined by the INSTANCE environment variable.
|
||||
* @returns {*}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceRegion = () =>
|
||||
InstanceManager({
|
||||
imex: "ca-central-1",
|
||||
rome: "us-east-2"
|
||||
});
|
||||
|
||||
exports.InstanceEndpoints = () =>
|
||||
/**
|
||||
* Checks if the instance is configured to use LocalStack by verifying the presence of the LOCALSTACK_HOSTNAME
|
||||
* environment variable.
|
||||
* @returns {boolean}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceIsLocalStackEnabled = () =>
|
||||
isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
/**
|
||||
* Returns the LocalStack endpoint URL based on the LOCALSTACK_HOSTNAME environment variable.
|
||||
* @returns {`http://${*}:4566`}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceLocalStackEndpoint = () => `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
|
||||
/**
|
||||
* Returns the appropriate endpoints for the current instance, which can be used for making API calls or other network
|
||||
* requests.
|
||||
* @returns {*|null}
|
||||
* @constructor
|
||||
*/
|
||||
const InstanceEndpoints = () =>
|
||||
InstanceManager({
|
||||
imex:
|
||||
process.env?.NODE_ENV === "development"
|
||||
@@ -74,4 +111,11 @@ exports.InstanceEndpoints = () =>
|
||||
: "https://romeonline.io"
|
||||
});
|
||||
|
||||
exports.default = InstanceManager;
|
||||
module.exports = {
|
||||
InstanceManager,
|
||||
InstanceRegion,
|
||||
InstanceIsLocalStackEnabled,
|
||||
InstanceLocalStackEndpoint,
|
||||
InstanceEndpoints,
|
||||
default: InstanceManager
|
||||
};
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
const winston = require("winston");
|
||||
const WinstonCloudWatch = require("winston-cloudwatch");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { uploadFileToS3 } = require("./s3");
|
||||
const { v4 } = require("uuid");
|
||||
const { InstanceRegion } = require("./instanceMgr");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("./instanceMgr");
|
||||
const getHostNameOrIP = require("./getHostNameOrIP");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const queries = require("../graphql-client/queries");
|
||||
@@ -48,7 +47,7 @@ const normalizeLevel = (level) => (level ? level.toLowerCase() : LOG_LEVELS.debu
|
||||
|
||||
const createLogger = () => {
|
||||
try {
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
const isLocal = InstanceIsLocalStackEnabled();
|
||||
const logGroupName = isLocal ? "development" : process.env.CLOUDWATCH_LOG_GROUP;
|
||||
|
||||
const winstonCloudwatchTransportDefaults = {
|
||||
@@ -60,7 +59,7 @@ const createLogger = () => {
|
||||
};
|
||||
|
||||
if (isLocal) {
|
||||
winstonCloudwatchTransportDefaults.awsOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
winstonCloudwatchTransportDefaults.awsOptions.endpoint = InstanceLocalStackEndpoint();
|
||||
}
|
||||
|
||||
const levelFilter = (levels) => {
|
||||
|
||||
@@ -7,8 +7,7 @@ const {
|
||||
CopyObjectCommand
|
||||
} = require("@aws-sdk/client-s3");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { InstanceRegion } = require("./instanceMgr");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { InstanceRegion, InstanceIsLocalStackEnabled, InstanceLocalStackEndpoint } = require("./instanceMgr");
|
||||
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||
|
||||
const createS3Client = () => {
|
||||
@@ -17,10 +16,8 @@ const createS3Client = () => {
|
||||
credentials: defaultProvider()
|
||||
};
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
if (isLocal) {
|
||||
S3Options.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
if (InstanceIsLocalStackEnabled()) {
|
||||
S3Options.endpoint = InstanceLocalStackEndpoint();
|
||||
S3Options.forcePathStyle = true; // Needed for LocalStack to avoid bucket name as hostname
|
||||
}
|
||||
|
||||
@@ -105,7 +102,7 @@ const createS3Client = () => {
|
||||
});
|
||||
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 360 });
|
||||
return presignedUrl;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
uploadFileToS3,
|
||||
@@ -119,7 +116,4 @@ const createS3Client = () => {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = createS3Client();
|
||||
|
||||
Reference in New Issue
Block a user