376 lines
13 KiB
JavaScript
376 lines
13 KiB
JavaScript
import { CopyFilled, DeleteFilled } from "@ant-design/icons";
|
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
|
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";
|
|
import { connect } from "react-redux";
|
|
import { createStructuredSelector } from "reselect";
|
|
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
|
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
|
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
|
import { getCurrentUser } from "../../firebase/firebase.utils";
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
cardPaymentModal: selectCardPayment,
|
|
bodyshop: selectBodyshop,
|
|
currentUser: getCurrentUser
|
|
});
|
|
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
|
dispatch(
|
|
insertAuditTrail({
|
|
jobid,
|
|
operation,
|
|
type
|
|
})
|
|
),
|
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
|
});
|
|
|
|
const CardPaymentModalComponent = ({
|
|
bodyshop,
|
|
currentUser,
|
|
cardPaymentModal,
|
|
toggleModalVisible,
|
|
insertAuditTrail
|
|
}) => {
|
|
const { context, actions } = cardPaymentModal;
|
|
|
|
const [form] = Form.useForm();
|
|
const [paymentLink, setPaymentLink] = useState();
|
|
const [loading, setLoading] = useState(false);
|
|
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
|
const { t } = useTranslation();
|
|
|
|
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
|
variables: { jobids: [context.jobid] },
|
|
skip: !context?.jobid
|
|
});
|
|
|
|
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(() => {
|
|
//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?.refetch) actions.refetch();
|
|
setLoading(false);
|
|
toggleModalVisible();
|
|
}, 750);
|
|
});
|
|
|
|
window.intellipay.runOnNonApproval(async (response) => {
|
|
// Mutate unsuccessful payment
|
|
|
|
const { payments } = form.getFieldsValue();
|
|
await insertPaymentResponse({
|
|
variables: {
|
|
paymentResponse: payments.map((payment) => ({
|
|
amount: payment.amount,
|
|
bodyshopid: bodyshop.id,
|
|
jobid: payment.jobid,
|
|
declinereason: response.declinereason,
|
|
ext_paymentid: response.paymentid.toString(),
|
|
successful: false,
|
|
response
|
|
}))
|
|
}
|
|
});
|
|
|
|
payments.forEach((payment) =>
|
|
insertAuditTrail({
|
|
jobid: payment.jobid,
|
|
operation: AuditTrailMapping.failedpayment(),
|
|
type: "failedpayment"
|
|
})
|
|
);
|
|
});
|
|
};
|
|
|
|
const handleIntelliPayCharge = async () => {
|
|
setLoading(true);
|
|
//Validate
|
|
try {
|
|
await form.validateFields();
|
|
} catch {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const iPayData = collectIPayFields();
|
|
const { payments } = form.getFieldsValue();
|
|
|
|
try {
|
|
const response = await axios.post("/intellipay/lightbox_credentials", {
|
|
bodyshop,
|
|
refresh: !!window.intellipay,
|
|
paymentSplitMeta: form.getFieldsValue(),
|
|
iPayData: iPayData,
|
|
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email }))
|
|
});
|
|
|
|
if (window.intellipay) {
|
|
// eslint-disable-next-line no-eval
|
|
eval(response.data);
|
|
pollForIntelliPay(() => {
|
|
SetIntellipayCallbackFunctions();
|
|
window.intellipay.autoOpen();
|
|
});
|
|
} else {
|
|
const rg = document.createRange();
|
|
const node = rg.createContextualFragment(response.data);
|
|
document.documentElement.appendChild(node);
|
|
pollForIntelliPay(() => {
|
|
SetIntellipayCallbackFunctions();
|
|
window.intellipay.isAutoOpen = true;
|
|
window.intellipay.initialize();
|
|
});
|
|
}
|
|
} catch (error) {
|
|
notification.open({
|
|
type: "error",
|
|
message: t("job_payments.notifications.error.openingip")
|
|
});
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleIntelliPayChargeShortLink = async () => {
|
|
setLoading(true);
|
|
//Validate
|
|
try {
|
|
await form.validateFields();
|
|
} 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) => 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(),
|
|
iPayData: iPayData
|
|
});
|
|
|
|
if (response?.data?.shorUrl) {
|
|
setPaymentLink(response.data.shorUrl);
|
|
await navigator.clipboard.writeText(response.data.shorUrl);
|
|
message.success(t("general.actions.copied"));
|
|
}
|
|
setLoading(false);
|
|
} catch (error) {
|
|
notification.open({
|
|
type: "error",
|
|
message: t("job_payments.notifications.error.openingip")
|
|
});
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card title="Card Payment">
|
|
<Spin spinning={loading}>
|
|
<Form
|
|
form={form}
|
|
layout="vertical"
|
|
initialValues={{
|
|
payments: context.jobid ? [{ jobid: context.jobid }] : []
|
|
}}
|
|
>
|
|
<Form.List name={["payments"]}>
|
|
{(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>
|
|
))}
|
|
<Form.Item>
|
|
<Button type="dashed" onClick={() => add()} style={{ width: "100%" }}>
|
|
{t("general.actions.add")}
|
|
</Button>
|
|
</Form.Item>
|
|
</div>
|
|
)}
|
|
</Form.List>
|
|
|
|
<Form.Item
|
|
shouldUpdate={(prevValues, curValues) =>
|
|
prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !==
|
|
curValues.payments?.map((p) => p?.jobid + p?.amount).join()
|
|
}
|
|
>
|
|
{() => {
|
|
//If all of the job ids have been fileld in, then query and update the IP field.
|
|
const { payments } = form.getFieldsValue();
|
|
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
|
refetch({ jobids: payments.map((p) => p.jobid) });
|
|
}
|
|
return (
|
|
<>
|
|
<Input
|
|
className="ipayfield"
|
|
data-ipayname="account"
|
|
type="hidden"
|
|
value={
|
|
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
|
}
|
|
/>
|
|
<Input
|
|
className="ipayfield"
|
|
data-ipayname="email"
|
|
type="hidden"
|
|
value={
|
|
payments && data && data.jobs.length > 0 ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea : null
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
<Form.Item
|
|
shouldUpdate={(prevValues, curValues) =>
|
|
prevValues.payments?.map((p) => p?.amount).join() !== curValues.payments?.map((p) => p?.amount).join()
|
|
}
|
|
>
|
|
{() => {
|
|
const { payments } = form.getFieldsValue();
|
|
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} />
|
|
<Input
|
|
className="ipayfield"
|
|
data-ipayname="amount"
|
|
type="hidden"
|
|
value={totalAmountToCharge?.toFixed(2)}
|
|
/>
|
|
<Input
|
|
className="ipayfield"
|
|
data-ipayname="comment"
|
|
type="hidden"
|
|
value={btoa(JSON.stringify(payments))}
|
|
hidden
|
|
/>
|
|
<Button
|
|
type="primary"
|
|
// data-ipayname="submit"
|
|
className="ipayfield"
|
|
loading={queryLoading || loading}
|
|
disabled={!(totalAmountToCharge > 0)}
|
|
onClick={handleIntelliPayCharge}
|
|
>
|
|
{t("job_payments.buttons.proceedtopayment")}
|
|
</Button>
|
|
<Space direction="vertical" align="center">
|
|
<Button
|
|
type="primary"
|
|
// data-ipayname="submit"
|
|
className="ipayfield"
|
|
loading={queryLoading || loading}
|
|
disabled={!(totalAmountToCharge > 0)}
|
|
onClick={handleIntelliPayChargeShortLink}
|
|
>
|
|
{t("job_payments.buttons.create_short_link")}
|
|
</Button>
|
|
</Space>
|
|
</Space>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</Form>
|
|
{paymentLink && (
|
|
<Space
|
|
style={{ cursor: "pointer", float: "right" }}
|
|
align="end"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(paymentLink);
|
|
message.success(t("general.actions.copied"));
|
|
}}
|
|
>
|
|
<div>{paymentLink}</div>
|
|
<CopyFilled />
|
|
</Space>
|
|
)}
|
|
</Spin>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent);
|
|
|
|
//Poll for window.IntelliPay.fixAmount for 5 seconds. If it doesn't come up, just try anyways to force the possible error.
|
|
function pollForIntelliPay(callbackFunction) {
|
|
const timeout = 5000;
|
|
const interval = 150; // Poll every 100 milliseconds
|
|
const startTime = Date.now();
|
|
|
|
function checkFixAmount() {
|
|
if (window.intellipay && window.intellipay.fixAmount !== undefined) {
|
|
callbackFunction();
|
|
return;
|
|
}
|
|
|
|
if (Date.now() - startTime >= timeout) {
|
|
console.log("Stopped polling IntelliPay after 10 seconds. Attemping to set functions anyways.");
|
|
callbackFunction();
|
|
return;
|
|
}
|
|
|
|
setTimeout(checkFixAmount, interval);
|
|
}
|
|
|
|
checkFixAmount();
|
|
}
|