Merged in release/2024-12-13 (pull request #2016)

Release/2024 12 13
This commit is contained in:
Patrick Fic
2024-12-12 17:48:33 +00:00
5 changed files with 320 additions and 176 deletions

View File

@@ -1,6 +1,6 @@
import { DeleteFilled, CopyFilled } from "@ant-design/icons";
import { CopyFilled, DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, message, notification } from "antd";
import { Button, Card, Col, Form, Input, message, notification, Row, Space, Spin, Statistic } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -23,7 +23,14 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
});
@@ -39,7 +46,6 @@ const CardPaymentModalComponent = ({
const [form] = Form.useForm();
const [paymentLink, setPaymentLink] = useState();
const [loading, setLoading] = useState(false);
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation();
@@ -48,24 +54,33 @@ const CardPaymentModalComponent = ({
skip: !context?.jobid
});
//Initialize the intellipay window.
const collectIPayFields = () => {
const iPayFields = document.querySelectorAll(".ipayfield");
const iPayData = {};
iPayFields.forEach((field) => {
iPayData[field.dataset.ipayname] = field.value;
});
return iPayData;
};
const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions.");
window.intellipay.runOnClose(() => {
//window.intellipay.initialize();
});
window.intellipay.runOnApproval(async function (response) {
window.intellipay.runOnApproval(() => {
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
//Add a slight delay to allow the refetch to properly get the data.
setTimeout(() => {
if (actions && actions.refetch && typeof actions.refetch === "function") actions.refetch();
if (actions?.refetch) actions.refetch();
setLoading(false);
toggleModalVisible();
}, 750);
});
window.intellipay.runOnNonApproval(async function (response) {
window.intellipay.runOnNonApproval(async (response) => {
// Mutate unsuccessful payment
const { payments } = form.getFieldsValue();
@@ -98,16 +113,19 @@ const CardPaymentModalComponent = ({
//Validate
try {
await form.validateFields();
} catch (error) {
} catch {
setLoading(false);
return;
}
const iPayData = collectIPayFields();
try {
const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop,
refresh: !!window.intellipay,
paymentSplitMeta: form.getFieldsValue()
paymentSplitMeta: form.getFieldsValue(),
iPayData: iPayData
});
if (window.intellipay) {
@@ -116,8 +134,8 @@ const CardPaymentModalComponent = ({
SetIntellipayCallbackFunctions();
window.intellipay.autoOpen();
} else {
var rg = document.createRange();
let node = rg.createContextualFragment(response.data);
const rg = document.createRange();
const node = rg.createContextualFragment(response.data);
document.documentElement.appendChild(node);
SetIntellipayCallbackFunctions();
window.intellipay.isAutoOpen = true;
@@ -137,25 +155,27 @@ const CardPaymentModalComponent = ({
//Validate
try {
await form.validateFields();
} catch (error) {
} catch {
setLoading(false);
return;
}
const iPayData = collectIPayFields();
try {
const { payments } = form.getFieldsValue();
const response = await axios.post("/intellipay/generate_payment_url", {
bodyshop,
amount: payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0),
account: payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
amount: payments.reduce((acc, val) => acc + (val?.amount || 0), 0),
account: payments && data?.jobs?.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })),
paymentSplitMeta: form.getFieldsValue()
paymentSplitMeta: form.getFieldsValue(),
iPayData: iPayData
});
if (response.data) {
setPaymentLink(response.data?.shorUrl);
navigator.clipboard.writeText(response.data?.shorUrl);
if (response?.data?.shorUrl) {
setPaymentLink(response.data.shorUrl);
await navigator.clipboard.writeText(response.data.shorUrl);
message.success(t("general.actions.copied"));
}
setLoading(false);
@@ -179,67 +199,44 @@ const CardPaymentModalComponent = ({
}}
>
<Form.List name={["payments"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Form.Item
key={`${index}jobid`}
label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<JobSearchSelectComponent notExported={false} clm_no />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
key={`${index}amount`}
label={t("payments.fields.amount")}
name={[field.name, "amount"]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<CurrencyFormItemComponent />
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{ margin: "1rem" }}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
{(fields, { add, remove }) => (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Form.Item
key={`${index}jobid`}
label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[{ required: true }]}
>
<JobSearchSelectComponent notExported={false} clm_no />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
key={`${index}amount`}
label={t("payments.fields.amount")}
name={[field.name, "amount"]}
rules={[{ required: true }]}
>
<CurrencyFormItemComponent />
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled style={{ margin: "1rem" }} onClick={() => remove(field.name)} />
</Col>
</Row>
</Form.Item>
</div>
);
}}
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} style={{ width: "100%" }}>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
)}
</Form.List>
<Form.Item
@@ -283,9 +280,7 @@ const CardPaymentModalComponent = ({
>
{() => {
const { payments } = form.getFieldsValue();
const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0);
const totalAmountToCharge = payments?.reduce((acc, val) => acc + (val?.amount || 0), 0);
return (
<Space style={{ float: "right" }}>
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />

View File

@@ -8,6 +8,7 @@ import { selectPrintCenter } from "../../redux/modals/modals.selectors";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GenerateDocument } from "../../utils/RenderTemplate";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter,
@@ -42,7 +43,12 @@ export function PrintCenterItemComponent({
setLoading(false);
};
if (disabled) return <li className="print-center-item">{item.title} </li>;
if (disabled || item.featureNameRestricted)
return (
<li className="print-center-item">
<LockWrapperComponent featureName={item.featureNameRestricted}>{item.title}</LockWrapperComponent>
</li>
);
return (
<li>
<Space wrap>

View File

@@ -21,6 +21,7 @@ import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
import "./report-center-modal.styles.scss";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter,
@@ -185,13 +186,23 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
</BlurWrapperComponent>
) : (
<ul style={{ listStyleType: "none", columns: "2 auto" }}>
{grouped[key].map((item) => (
<li key={item.key}>
<Radio key={item.key} value={item.key}>
{item.title}
</Radio>
</li>
))}
{grouped[key].map((item) =>
item.featureNameRestricted ? (
<li key={item.key}>
<LockWrapperComponent featureName={item.featureNameRestricted}>
<Radio key={item.key} value={item.key}>
{item.title}
</Radio>
</LockWrapperComponent>
</li>
) : (
<li key={item.key}>
<Radio key={item.key} value={item.key}>
{item.title}
</Radio>
</li>
)
)}
</ul>
)}
</Card.Grid>
@@ -297,16 +308,13 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
},
{
validator: (_, value) => {
if (
!import.meta.env.VITE_APP_IS_TEST &&
import.meta.env.PROD &&
value &&
value[0] &&
value[1]
) {
const diffInDays = (value[1] - value[0]) / (1000 * 3600 * 24);
if (diffInDays > 92) {
return Promise.reject(t("general.validation.dateRangeExceeded"));
if (value && value[0] && value[1]) {
const relatedRestrictedReport = restrictedReports.find((r) => r.key === key);
if (relatedRestrictedReport) {
const diffInDays = (value[1] - value[0]) / (1000 * 3600 * 24);
if (diffInDays > relatedRestrictedReport.days) {
return Promise.reject(t("general.validation.dateRangeExceeded"));
}
}
}
return Promise.resolve();
@@ -369,3 +377,14 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
</div>
);
}
const restrictedReports = [
{ key: "job_costing_ro", days: 183 },
{ key: "job_costing_ro_date_summary", days: 183 },
{ key: "job_costing_ro_csr", days: 183 },
{ key: "job_costing_ro_ins_co", days: 183 },
{ key: "job_costing_ro_date_detail", days: 183 },
{ key: "job_costing_ro_estimator", days: 183 },
{ key: "job_lifecycle_date_detail", days: 183 },
{ key: "job_lifecycle_date_summary", days: 183 }
];

View File

@@ -342,7 +342,8 @@ export const TemplateList = (type, context) => {
subject: i18n.t("printcenter.jobs.purchases_by_ro_detail"),
key: "purchases_by_ro_detail",
disabled: false,
group: "financial"
group: "financial",
featureNameRestricted: "bills"
},
purchases_by_ro_summary: {
title: i18n.t("printcenter.jobs.purchases_by_ro_summary"),
@@ -350,7 +351,8 @@ export const TemplateList = (type, context) => {
subject: i18n.t("printcenter.jobs.purchases_by_ro_summary"),
key: "purchases_by_ro_summary",
disabled: false,
group: "financial"
group: "financial",
featureNameRestricted: "bills"
},
filing_coversheet_portrait: {
title: i18n.t("printcenter.jobs.filing_coversheet_portrait"),
@@ -398,7 +400,8 @@ export const TemplateList = (type, context) => {
key: "csi_invitation",
subject: i18n.t("printcenter.jobs.csi_invitation"),
disabled: false,
group: "post"
group: "post",
featureNameRestricted: "csi"
},
window_tag_sublet: {
title: i18n.t("printcenter.jobs.window_tag_sublet"),
@@ -549,7 +552,8 @@ export const TemplateList = (type, context) => {
subject: i18n.t("printcenter.jobs.timetickets_ro"),
key: "timetickets_ro",
disabled: false,
group: "financial"
group: "financial",
featureNameRestricted: "timetickets"
},
dms_posting_sheet: {
title: i18n.t("printcenter.jobs.dms_posting_sheet"),
@@ -576,7 +580,8 @@ export const TemplateList = (type, context) => {
key: "committed_timetickets_ro",
disabled: false,
group: "financial",
enhanced_payroll: true
enhanced_payroll: true,
featureNameRestricted: "timetickets"
},
job_lifecycle_ro: {
title: i18n.t("printcenter.jobs.job_lifecycle_ro"),
@@ -584,7 +589,8 @@ export const TemplateList = (type, context) => {
subject: i18n.t("printcenter.jobs.job_lifecycle_ro"),
key: "job_lifecycle_ro",
disabled: false,
group: "post"
group: "post",
featureNameRestricted: "lifecycle"
},
job_tasks: {
title: i18n.t("printcenter.jobs.job_tasks"),
@@ -628,7 +634,8 @@ export const TemplateList = (type, context) => {
description: "",
key: "csi_invitation_action",
subject: i18n.t("printcenter.jobs.csi_invitation_action"),
disabled: false
disabled: false,
featureNameRestricted: "csi"
},
individual_job_note: {
title: i18n.t("printcenter.jobs.individual_job_note"),
@@ -1606,7 +1613,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.exportlogs"),
field: i18n.t("exportlogs.fields.createdat")
},
group: "customers"
group: "customers",
featureNameRestricted: "export"
},
export_receivables: {
title: i18n.t("reportcenter.templates.export_receivables"),
@@ -1619,7 +1627,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.exportlogs"),
field: i18n.t("exportlogs.fields.createdat")
},
group: "sales"
group: "sales",
featureNameRestricted: "export"
},
parts_backorder: {
title: i18n.t("reportcenter.templates.parts_backorder"),
@@ -1684,7 +1693,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open")
},
group: "jobs"
group: "jobs",
featureNameRestricted: "timetickets"
},
work_in_progress_committed_labour: {
title: i18n.t("reportcenter.templates.work_in_progress_committed_labour"),
@@ -1698,7 +1708,8 @@ export const TemplateList = (type, context) => {
field: i18n.t("jobs.fields.date_open")
},
group: "jobs",
enhanced_payroll: true
enhanced_payroll: true,
featureNameRestricted: "timetickets"
},
work_in_progress_payables: {
title: i18n.t("reportcenter.templates.work_in_progress_payables"),
@@ -1711,7 +1722,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open")
},
group: "jobs"
group: "jobs",
featureNameRestricted: "bills"
},
lag_time: {
title: i18n.t("reportcenter.templates.lag_time"),
@@ -1802,7 +1814,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.csi"),
field: i18n.t("csi.fields.created_at") // Also date invoice.
},
group: "customers"
group: "customers",
featureNameRestricted: "csi"
},
estimates_written_converted: {
title: i18n.t("reportcenter.templates.estimates_written_converted"),
@@ -1839,7 +1852,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced")
},
group: "jobs"
group: "jobs",
featureNameRestricted: "bills"
},
parts_received_not_scheduled: {
title: i18n.t("reportcenter.templates.parts_received_not_scheduled"),
@@ -1935,7 +1949,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced")
},
group: "jobs"
group: "jobs",
featureNameRestricted: "export"
},
purchase_return_ratio_grouped_by_vendor_detail: {
title: i18n.t("reportcenter.templates.purchase_return_ratio_grouped_by_vendor_detail"),
@@ -1996,7 +2011,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_exported")
},
group: "sales"
group: "sales",
featureNameRestricted: "export"
},
exported_gsr_by_ro_labor: {
title: i18n.t("reportcenter.templates.exported_gsr_by_ro_labor"),
@@ -2009,7 +2025,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_exported")
},
group: "sales"
group: "sales",
featureNameRestricted: "export"
},
jobs_scheduled_completion: {
title: i18n.t("reportcenter.templates.jobs_scheduled_completion"),
@@ -2119,7 +2136,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced")
},
group: "jobs"
group: "jobs",
featureNameRestricted: "lifecycle"
},
job_lifecycle_date_summary: {
title: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
@@ -2131,7 +2149,8 @@ export const TemplateList = (type, context) => {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced")
},
group: "jobs"
group: "jobs",
featureNameRestricted: "lifecycle"
},
tasks_date: {
title: i18n.t("reportcenter.templates.tasks_date"),

View File

@@ -1,4 +1,3 @@
const GraphQLClient = require("graphql-request").GraphQLClient;
const path = require("path");
const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
@@ -6,7 +5,6 @@ const qs = require("query-string");
const axios = require("axios");
const moment = require("moment");
const logger = require("../utils/logger");
const InstanceManager = require("../utils/instanceMgr").default;
const { sendTaskEmail } = require("../email/sendemail");
const generateEmailTemplate = require("../email/generateTemplate");
const { getEndpoints } = require("../email/tasksEmails");
@@ -53,14 +51,19 @@ const getShopCredentials = async (bodyshop) => {
};
exports.lightbox_credentials = async (req, res) => {
logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, null);
logger.log("intellipay-lightbox-credentials", "DEBUG", req.user?.email, null, {
iPayData: req.body.iPayData,
bodyshop: req.body.bodyshop
});
const shopCredentials = await getShopCredentials(req.body.bodyshop);
if (shopCredentials.error) {
logger.log("intellipay-credentials-error", "ERROR", req.user?.email, null, { message: shopCredentials.error });
res.json(shopCredentials);
return;
}
try {
const options = {
method: "POST",
@@ -74,26 +77,39 @@ exports.lightbox_credentials = async (req, res) => {
const response = await axios(options);
logger.log("intellipay-lightbox-success", "DEBUG", req.user?.email, null, {
responseData: response.data,
requestOptions: options
});
res.send(response.data);
} catch (error) {
//console.log(error);
logger.log("intellipay-lightbox-credentials-error", "ERROR", req.user?.email, null, {
error: JSON.stringify(error)
});
res.json({ error });
logger.log("intellipay-lightbox-error", "ERROR", req.user?.email, null, { message: error.message });
res.json({ message: error.message });
}
};
exports.payment_refund = async (req, res) => {
logger.log("intellipay-refund", "DEBUG", req.user?.email, null, null);
logger.log("intellipay-refund-request-received", "DEBUG", req.user?.email, null, {
bodyshop: req.body.bodyshop,
paymentid: req.body.paymentid,
amount: req.body.amount
});
const shopCredentials = await getShopCredentials(req.body.bodyshop);
if (shopCredentials.error) {
logger.log("intellipay-refund-credentials-error", "ERROR", req.user?.email, null, {
credentialsError: shopCredentials.error
});
res.status(400).json({ error: shopCredentials.error });
return;
}
try {
const options = {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
data: qs.stringify({
method: "payment_refund",
...shopCredentials,
@@ -103,132 +119,216 @@ exports.payment_refund = async (req, res) => {
url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund`
};
logger.log("intellipay-refund-options-prepared", "DEBUG", req.user?.email, null, {
options
});
const response = await axios(options);
logger.log("intellipay-refund-success", "DEBUG", req.user?.email, null, {
responseData: response.data
});
res.send(response.data);
} catch (error) {
//console.log(error);
logger.log("intellipay-refund-error", "ERROR", req.user?.email, null, {
error: JSON.stringify(error)
error: error.message,
stack: error.stack,
requestOptions: {
paymentid: req.body.paymentid,
amount: req.body.amount
}
});
res.json({ error });
res.status(500).json({ error: error.message });
}
};
exports.generate_payment_url = async (req, res) => {
logger.log("intellipay-payment-url", "DEBUG", req.user?.email, null, null);
logger.log("intellipay-generate-payment-url-received", "DEBUG", req.user?.email, null, {
bodyshop: req.body.bodyshop,
amount: req.body.amount,
account: req.body.account,
comment: req.body.comment,
invoice: req.body.invoice
});
const shopCredentials = await getShopCredentials(req.body.bodyshop);
if (shopCredentials.error) {
logger.log("intellipay-generate-payment-url-credentials-error", "ERROR", req.user?.email, null, {
credentialsError: shopCredentials.error
});
res.status(400).json({ error: shopCredentials.error });
return;
}
try {
const options = {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
//TODO: Move these to environment variables/database.
data: qs.stringify({
...shopCredentials,
//...req.body,
amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat("0.00"),
account: req.body.account,
comment: req.body.comment,
invoice: req.body.invoice,
createshorturl: true
//The postback URL is set at the CP teller global terminal settings page.
}),
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url`
};
logger.log("intellipay-generate-payment-url-options-prepared", "DEBUG", req.user?.email, null, {
options
});
const response = await axios(options);
logger.log("intellipay-generate-payment-url-success", "DEBUG", req.user?.email, null, {
responseData: response.data
});
res.send(response.data);
} catch (error) {
//console.log(error);
logger.log("intellipay-payment-url-error", "ERROR", req.user?.email, null, {
error: JSON.stringify(error)
logger.log("intellipay-generate-payment-url-error", "ERROR", req.user?.email, null, {
error: error.message,
stack: error.stack,
requestOptions: {
amount: req.body.amount,
account: req.body.account,
comment: req.body.comment,
invoice: req.body.invoice
}
});
res.json({ error });
res.status(500).json({ error: error.message });
}
};
//Reference: https://intellipay.com/dist/webapi26.html#operation/fee
exports.checkfee = async (req, res) => {
// Requires amount, bodyshop.imexshopid, and state? to get data.
logger.log("intellipay-fee-check", "DEBUG", req.user?.email, null, null);
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, {
bodyshop: req.body.bodyshop,
amount: req.body.amount
});
//If there's no amount, there can't be a fee. Skip the call.
if (!req.body.amount || req.body.amount <= 0) {
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
message: "Amount is zero or undefined, skipping fee check."
});
res.json({ fee: 0 });
return;
}
const shopCredentials = await getShopCredentials(req.body.bodyshop);
if (shopCredentials.error) {
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
credentialsError: shopCredentials.error
});
res.status(400).json({ error: shopCredentials.error });
return;
}
try {
const options = {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
//TODO: Move these to environment variables/database.
data: qs.stringify(
{
method: "fee",
...shopCredentials,
amount: req.body.amount,
paymenttype: `CC`,
cardnum: "4111111111111111", //Not needed per documentation, but incorrect values come back without it.
cardnum: "4111111111111111", // Required for compatibility with API
state:
req.body.bodyshop?.state && req.body.bodyshop.state?.length === 2
req.body.bodyshop?.state && req.body.bodyshop.state.length === 2
? req.body.bodyshop.state.toUpperCase()
: "ZZ" //Same as above
: "ZZ"
},
{ sort: false } //ColdFusion Query Strings depend on order. This preserves it.
{ sort: false } // Ensure query string order is preserved
),
url: `https://${domain}.cpteller.com/api/26/webapi.cfc`
};
logger.log("intellipay-checkfee-options-prepared", "DEBUG", req.user?.email, null, {
options
});
const response = await axios(options);
if (response.data?.error) {
logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, {
apiError: response.data.error
});
res.status(400).json({ error: response.data.error });
} else if (response.data < 0) {
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, "Fee amount returned is negative.");
res.json({ error: "Fee amount negative. Check API credentials & account configuration." });
} else {
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
fee: response.data
});
res.json({ fee: response.data });
}
} catch (error) {
//console.log(error);
logger.log("intellipay-fee-check-error", "ERROR", req.user?.email, null, {
error: error.message
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
error: error.message,
stack: error.stack,
amount: req.body.amount
});
res.status(400).json({ error });
res.status(500).json({ error: error.message });
}
};
exports.postback = async (req, res) => {
try {
logger.log("intellipay-postback", "DEBUG", req.user?.email, null, req.body);
const { body: values } = req;
const comment = Buffer.from(values?.comment, "base64").toString();
logger.log("intellipay-postback-received", "DEBUG", req.user?.email, null, {
iprequest: values,
base64Comment: values?.comment || null
});
if ((!values.invoice || values.invoice === "") && !comment) {
// Decode the base64 comment, if it exists
const decodedComment = values?.comment ? Buffer.from(values.comment, "base64").toString() : null;
logger.log("intellipay-postback-decoded-comment", "DEBUG", req.user?.email, null, {
decodedComment
});
if ((!values.invoice || values.invoice === "") && !decodedComment) {
//invoice is specified through the pay link. Comment by IO.
logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, req.body);
logger.log("intellipay-postback-ignored", "DEBUG", req.user?.email, null, {
reason: "No invoice or comment provided",
iprequest: values
});
res.sendStatus(200);
return;
}
if (comment) {
if (decodedComment) {
//Shifted the order to have this first to retain backwards compatibility for the old style of short link.
//This has been triggered by IO and may have multiple jobs.
const parsedComment = JSON.parse(comment);
const parsedComment = JSON.parse(decodedComment);
logger.log("intellipay-postback-parsed-comment", "DEBUG", req.user?.email, null, {
parsedComment
});
//Adding in the user email to the short pay email.
//Need to check this to ensure backwards compatibility for clients that don't update.
const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments;
// Fetch jobs by job IDs
const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, {
ids: partialPayments.map((p) => p.jobid)
});
logger.log("intellipay-postback-jobs-fetched", "DEBUG", req.user?.email, null, {
jobs
});
// Insert new payments
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
paymentInput: partialPayments.map((p) => ({
amount: p.amount,
@@ -250,13 +350,12 @@ exports.postback = async (req, res) => {
}
}))
});
logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, JSON.stringify(jobs), {
iprequest: values,
logger.log("intellipay-postback-payment-success", "DEBUG", req.user?.email, null, {
paymentResult
});
if (values.origin === "OneLink" && parsedComment.userEmail) {
//Send an email, it was a text to pay link.
try {
const endPoints = getEndpoints();
sendTaskEmail({
@@ -275,20 +374,23 @@ exports.postback = async (req, res) => {
})
});
} catch (error) {
logger.log("intellipay-postback-app-email-error", "DEBUG", req.user?.email, JSON.stringify(jobs), {
iprequest: values,
paymentResult,
error: error.message
logger.log("intellipay-postback-email-error", "ERROR", req.user?.email, null, {
error: error.message,
jobs,
paymentResult
});
}
}
res.sendStatus(200);
} else if (values.invoice) {
//This is a link email that's been sent out.
const job = await gqlClient.request(queries.GET_JOB_BY_PK, {
id: values.invoice
});
logger.log("intellipay-postback-invoice-job-fetched", "DEBUG", req.user?.email, null, {
job
});
const paymentResult = await gqlClient.request(queries.INSERT_NEW_PAYMENT, {
paymentInput: {
amount: values.total,
@@ -300,6 +402,10 @@ exports.postback = async (req, res) => {
}
});
logger.log("intellipay-postback-invoice-payment-success", "DEBUG", req.user?.email, null, {
paymentResult
});
const responseResults = await gqlClient.request(queries.INSERT_PAYMENT_RESPONSE, {
paymentResponse: {
amount: values.total,
@@ -313,18 +419,17 @@ exports.postback = async (req, res) => {
}
});
logger.log("intellipay-postback-link-success", "DEBUG", req.user?.email, values.invoice, {
iprequest: values,
responseResults,
paymentResult
logger.log("intellipay-postback-invoice-response-success", "DEBUG", req.user?.email, null, {
responseResults
});
res.sendStatus(200);
}
} catch (error) {
logger.log("intellipay-postback-total-error", "ERROR", req.user?.email, null, {
error: JSON.stringify(error),
body: req.body
logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, {
error: error.message,
stack: error.stack,
iprequest: req.body
});
res.status(400).json({ succesful: false, error: error.message });
res.status(400).json({ successful: false, error: error.message });
}
};