Merge branch 'feature/intellipay' into release/2023-09-15

This commit is contained in:
Patrick Fic
2023-09-15 10:38:51 -07:00
9 changed files with 281 additions and 157 deletions

View File

@@ -37301,6 +37301,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>inserting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>

View File

@@ -1,12 +1,15 @@
import { useMutation, useQuery } from "@apollo/client"; import { DeleteFilled } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { import {
Button, Button,
Card, Card,
Col,
Form, Form,
Input, Input,
InputNumber,
Row, Row,
Space,
Spin, Spin,
Statistic,
notification, notification,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
@@ -17,7 +20,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { import {
INSERT_PAYMENT_RESPONSE, INSERT_PAYMENT_RESPONSE,
QUERY_RO_AND_OWNER_BY_JOB_PK, QUERY_RO_AND_OWNER_BY_JOB_PKS,
} from "../../graphql/payment_response.queries"; } from "../../graphql/payment_response.queries";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries"; import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
@@ -25,9 +28,8 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCardPayment } from "../../redux/modals/modals.selectors"; import { selectCardPayment } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import DataLabel from "../data-label/data-label.component"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
cardPaymentModal: selectCardPayment, cardPaymentModal: selectCardPayment,
@@ -49,18 +51,21 @@ const CardPaymentModalComponent = ({
const { context } = cardPaymentModal; const { context } = cardPaymentModal;
const [form] = Form.useForm(); const [form] = Form.useForm();
const amount = Form.useWatch("amount", form);
const jobid = Form.useWatch("jobid", form);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { t } = useTranslation(); const { t } = useTranslation();
const { data, refetch } = useQuery(QUERY_RO_AND_OWNER_BY_JOB_PK, { const [, { data, refetch, queryLoading }] = useLazyQuery(
variables: { jobid: context?.jobid ?? "" }, QUERY_RO_AND_OWNER_BY_JOB_PKS,
skip: !context?.jobid, {
}); variables: { jobids: [context.jobid] },
skip: true,
}
);
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
//Initialize the intellipay window. //Initialize the intellipay window.
const SetIntellipayCallbackFunctions = () => { const SetIntellipayCallbackFunctions = () => {
console.log("*** Set IntelliPay callback functions."); console.log("*** Set IntelliPay callback functions.");
@@ -76,69 +81,68 @@ const CardPaymentModalComponent = ({
window.intellipay.runOnNonApproval(async function (response) { window.intellipay.runOnNonApproval(async function (response) {
// Mutate unsuccessful payment // Mutate unsuccessful payment
const { payments } = form.getFieldsValue();
await insertPaymentResponse({ await insertPaymentResponse({
variables: { variables: {
paymentResponse: { paymentResponse: payments.map((payment) => ({
amount: response.amount, amount: payment.amount,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
jobid: jobid || context.jobid, jobid: payment.jobid,
declinereason: response.declinereason, declinereason: response.declinereason,
ext_paymentid: response.paymentid.toString(), ext_paymentid: response.paymentid.toString(),
successful: false, successful: false,
response, response,
}, })),
}, },
}); });
payments.forEach((payment) =>
insertAuditTrail({ insertAuditTrail({
jobid: jobid || context?.jobid, jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(), operation: AuditTrailMapping.failedpayment(),
}); })
);
}); });
}; };
const handleFinish = async (values) => { const handleFinish = async (values) => {
try { try {
const paymentResult = await insertPayment({ await insertPayment({
variables: { variables: {
paymentInput: { paymentInput: values.payments.map((payment) => ({
amount: values.amount, amount: payment.amount,
transactionid: (values.paymentResponse.paymentid || "").toString(), transactionid: (values.paymentResponse.paymentid || "").toString(),
payer: t("payments.labels.customer"), payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand, type: values.paymentResponse.cardbrand,
jobid: values.jobid, jobid: payment.jobid,
date: moment(Date.now()), date: moment(Date.now()),
}, payment_responses: {
}, data: [
refetchQueries: ["GET_JOB_BY_PK"], {
update(cache, { data }) { amount: payment.amount,
cache.modify({
id: cache.identify({ id: values.jobid, __typename: "jobs" }),
fields: {
payments(cachedPayments) {
return [...data.insert_payments.returning, ...cachedPayments];
},
},
});
},
});
await insertPaymentResponse({
variables: {
paymentResponse: {
amount: values.amount,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
paymentid: paymentResult.data.insert_payments.returning[0].id,
jobid: values.jobid, jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason, declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(), ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true, successful: true,
response: values.paymentResponse, response: values.paymentResponse,
}, },
],
}, },
})),
},
refetchQueries: ["GET_JOB_BY_PK"],
}); });
toggleModalVisible(); toggleModalVisible();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
notification.open({
type: "error",
message: t("payments.errors.inserting", { error: error.message }),
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -146,9 +150,16 @@ const CardPaymentModalComponent = ({
const handleIntelliPayCharge = async () => { const handleIntelliPayCharge = async () => {
setLoading(true); setLoading(true);
try {
console.warn("*** Window.Intellipay", !!window.intellipay);
//Validate
try {
await form.validateFields();
} catch (error) {
setLoading(false);
return;
}
try {
const response = await axios.post("/intellipay/lightbox_credentials", { const response = await axios.post("/intellipay/lightbox_credentials", {
bodyshop, bodyshop,
refresh: !!window.intellipay, refresh: !!window.intellipay,
@@ -182,93 +193,175 @@ const CardPaymentModalComponent = ({
<Form <Form
onFinish={handleFinish} onFinish={handleFinish}
form={form} form={form}
initialValues={{ jobid: context?.jobid }} layout="vertical"
initialValues={{
payments: context.jobid ? [{ jobid: context.jobid }] : [],
}}
> >
<LayoutFormRow grow noDivider> <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 <Form.Item
name="jobid" key={`${index}jobid`}
label={t("bills.fields.ro_number")} label={t("jobs.fields.ro_number")}
name={[field.name, "jobid"]}
rules={[ rules={[
{ {
required: true, required: true,
// message: t("general.validation.required"), //message: t("general.validation.required"),
}, },
]} ]}
> >
<JobSearchSelectComponent <JobSearchSelectComponent
disabled={context?.jobid}
notExported={false} notExported={false}
clm_no clm_no
onChange={(e) => {
refetch({ jobid: e });
}}
/> />
</Form.Item> </Form.Item>
</LayoutFormRow> </Col>
<Col span={6}>
{/* Lighbox Input amount needs to be hidden */} <Form.Item
<Input key={`${index}amount`}
className="ipayfield" label={t("payments.fields.amount")}
data-ipayname="amount" name={[field.name, "amount"]}
type="hidden" rules={[
value={amount} {
hidden 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>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.payments?.map((p) => p?.jobid).join() !==
curValues.payments?.map((p) => p?.jobid).join()
}
>
{() => {
console.log("Updating the owner info section.");
//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
) {
console.log("**Calling refetch.");
refetch({ jobids: payments.map((p) => p.jobid) });
}
console.log(
"Acc info",
data,
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
);
return (
<>
<Input <Input
className="ipayfield" className="ipayfield"
data-ipayname="account" data-ipayname="account"
type="hidden" //type="hidden"
value={data?.jobs_by_pk.ro_number} value={
payments && data && data.jobs.length > 0
? data.jobs.map((j) => j.ro_number).join(", ")
: null
}
hidden hidden
/> />
<Input <Input
className="ipayfield" className="ipayfield"
data-ipayname="email" data-ipayname="email"
type="hidden" // type="hidden"
value={data?.jobs_by_pk.owner.ownr_ea} value={
payments && data && data.jobs.length > 0
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
: null
}
hidden hidden
/> />
{/* Lightbox payment response when it is completed */} </>
<Form.Item name="paymentResponse" hidden> );
<Input type="hidden" value={amount} /> }}
</Form.Item> </Form.Item>
<LayoutFormRow grow>
<Form.Item <Form.Item
label="Amount" shouldUpdate={(prevValues, curValues) =>
name="amount" prevValues.payments?.map((p) => p?.amount).join() !==
rules={[ curValues.payments?.map((p) => p?.amount).join()
{ }
required: true,
// message: t("general.validation.required"),
},
]}
> >
<InputNumber /> {() => {
</Form.Item> const { payments } = form.getFieldsValue();
const totalAmountToCharge = payments?.reduce((acc, val) => {
return acc + (val?.amount || 0);
}, 0);
<Row justify="space-around"> 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)}
hidden
/>
<Button <Button
type="primary" type="primary"
// data-ipayname="submit" // data-ipayname="submit"
className="ipayfield" className="ipayfield"
disabled={!amount || !jobid} loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayCharge} onClick={handleIntelliPayCharge}
> >
{t("job_payments.buttons.proceedtopayment")} {t("job_payments.buttons.proceedtopayment")}
</Button> </Button>
{context?.balance && ( </Space>
<DataLabel );
valueStyle={{
color: context?.balance.getAmount() !== 0 ? "red" : "green",
}} }}
label={t("payments.labels.balance")} </Form.Item>
>
{context?.balance.toFormat()} {/* Lightbox payment response when it is completed */}
</DataLabel> <Form.Item name="paymentResponse" hidden>
)} <Input type="hidden" />
</Row> </Form.Item>
</LayoutFormRow>
</Form> </Form>
</Spin> </Spin>
</Card> </Card>

View File

@@ -43,7 +43,7 @@ function CardPaymentModalContainer({
{t("job_payments.buttons.goback")} {t("job_payments.buttons.goback")}
</Button>, </Button>,
]} ]}
width="60%" width="80%"
destroyOnClose destroyOnClose
> >
<CardPaymentModalComponent /> <CardPaymentModalComponent />

View File

@@ -254,7 +254,7 @@ function Header({
onClick={() => { onClick={() => {
setCardPaymentContext({ setCardPaymentContext({
actions: {}, actions: {},
context: null, context: {},
}); });
}} }}
icon={<Icon component={FaCreditCard} />} icon={<Icon component={FaCreditCard} />}

View File

@@ -1,5 +1,12 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { Button, Descriptions, InputNumber, Modal, notification } from "antd"; import {
Button,
Descriptions,
InputNumber,
Modal,
Space,
notification,
} from "antd";
import axios from "axios"; import axios from "axios";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -158,6 +165,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
</Descriptions.Item> </Descriptions.Item>
{payment_response && ( {payment_response && (
<Descriptions.Item label={t("job_payments.titles.refundamount")}> <Descriptions.Item label={t("job_payments.titles.refundamount")}>
<Space>
<InputNumber <InputNumber
onChange={setRefundAmount} onChange={setRefundAmount}
max={max_refundable_amount} max={max_refundable_amount}
@@ -167,6 +175,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
<Button onClick={() => showConfirm(payment_response)}> <Button onClick={() => showConfirm(payment_response)}>
{t("job_payments.buttons.refundpayment")} {t("job_payments.buttons.refundpayment")}
</Button> </Button>
</Space>
</Descriptions.Item> </Descriptions.Item>
)} )}
</Descriptions> </Descriptions>

View File

@@ -28,18 +28,16 @@ export const QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID = gql`
} }
`; `;
export const QUERY_RO_AND_OWNER_BY_JOB_PK = gql` export const QUERY_RO_AND_OWNER_BY_JOB_PKS = gql`
query QUERY_RO_AND_OWNER_BY_JOB_PK($jobid: uuid!) { query QUERY_RO_AND_OWNER_BY_JOB_PKS($jobids: [uuid!]!) {
jobs_by_pk(id: $jobid) { jobs(where: { id: { _in: $jobids } }) {
ro_number ro_number
owner {
ownr_fn ownr_fn
ownr_ln ownr_ln
ownr_ea ownr_ea
ownr_zip ownr_zip
} }
} }
}
`; `;
export const GET_REFUNDABLE_AMOUNT_BY_JOBID = gql` export const GET_REFUNDABLE_AMOUNT_BY_JOBID = gql`

View File

@@ -2208,7 +2208,8 @@
}, },
"errors": { "errors": {
"exporting": "Error exporting payment(s). {{error}}", "exporting": "Error exporting payment(s). {{error}}",
"exporting-partner": "Error exporting to partner. Please check the partner interaction log for more errors." "exporting-partner": "Error exporting to partner. Please check the partner interaction log for more errors.",
"inserting": "Error inserting payment. {{error}}"
}, },
"fields": { "fields": {
"amount": "Amount", "amount": "Amount",

View File

@@ -2208,7 +2208,8 @@
}, },
"errors": { "errors": {
"exporting": "", "exporting": "",
"exporting-partner": "" "exporting-partner": "",
"inserting": ""
}, },
"fields": { "fields": {
"amount": "", "amount": "",

View File

@@ -2208,7 +2208,8 @@
}, },
"errors": { "errors": {
"exporting": "", "exporting": "",
"exporting-partner": "" "exporting-partner": "",
"inserting": ""
}, },
"fields": { "fields": {
"amount": "", "amount": "",