Files
bodyshop/client/src/components/card-payment-modal/card-payment-modal.component.jsx

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