Compare commits

...

22 Commits

Author SHA1 Message Date
Dave
6bda497d8c feature/IO-3647-Reynolds-Integration-Phase-2 - Enhance early RO with meaningful amounts. 2026-04-09 13:54:48 -04:00
Dave Richer
a4dbc5250e Merged in release/2026-04-03 (pull request #3179)
Release/2026-04-03 - IO-1366, IO-3356, IO-3515, IO-3587, IO-3599, IO-3609, IO-3616, IO-3622, IO-3623, IO-3627, IO-3629, IO-3637
2026-04-03 01:46:11 +00:00
Allan Carr
a1d0e2df93 Merged in feature/IO-3637-DMS-ID-Production-Board-Column (pull request #3175)
IO-3637 DMS ID Production Board Column

Approved-by: Dave Richer
Approved-by: Patrick Fic
2026-04-03 01:32:25 +00:00
Allan Carr
9a86a337bb IO-3637 DMS ID Production Board Column
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-04-02 15:50:56 -07:00
Dave
7688f22161 release/2026-04-03 - Clean up localstack endpoints / env check 2026-04-01 14:39:34 -04:00
Allan Carr
efdcd06921 Merged in feature/IO-1366-Re-export-Bill-Audit-Log (pull request #3170)
IO-1366 Bill Reexport Audit Log

Approved-by: Dave Richer
2026-03-31 20:18:44 +00: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
28 changed files with 524 additions and 150 deletions

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

@@ -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&apos;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." }}

View File

@@ -58,6 +58,7 @@ export function ProductionColumnsComponent({
const columnKeys = columns.map((i) => i.key);
const cols = dataSource({
bodyshop,
technician,
data,
state: tableState,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

View File

@@ -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.",

View File

@@ -121,6 +121,7 @@
"assignedlinehours": "",
"billdeleted": "",
"billposted": "",
"billmarkforreexport": "",
"billupdated": "",
"failedpayment": "",
"jobassignmentchange": "",

View File

@@ -120,6 +120,7 @@
"appointmentinsert": "",
"assignedlinehours": "",
"billdeleted": "",
"billmarkforreexport": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",

View File

@@ -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 }),

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

@@ -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);

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

@@ -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}`;

View File

@@ -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);

View File

@@ -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}`);
}

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)),

View File

@@ -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,

View File

@@ -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.

View File

@@ -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
};

View 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"
}
}
]
});
});
});

View File

@@ -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
};

View File

@@ -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) => {

View File

@@ -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();