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,
}, })),
}, },
}); });
insertAuditTrail({
jobid: jobid || context?.jobid, payments.forEach((payment) =>
operation: AuditTrailMapping.failedpayment(), insertAuditTrail({
}); jobid: payment.jobid,
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: [
{
amount: payment.amount,
bodyshopid: bodyshop.id,
jobid: payment.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
],
},
})),
}, },
refetchQueries: ["GET_JOB_BY_PK"], refetchQueries: ["GET_JOB_BY_PK"],
update(cache, { data }) {
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,
paymentid: paymentResult.data.insert_payments.returning[0].id,
jobid: values.jobid,
declinereason: values.paymentResponse.declinereason,
ext_paymentid: values.paymentResponse.paymentid.toString(),
successful: true,
response: values.paymentResponse,
},
},
}); });
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"]}>
<Form.Item {(fields, { add, remove, move }) => {
name="jobid" return (
label={t("bills.fields.ro_number")} <div>
rules={[ {fields.map((field, index) => (
{ <Form.Item key={field.key}>
required: true, <Row gutter={[16, 16]}>
// message: t("general.validation.required"), <Col span={16}>
}, <Form.Item
]} key={`${index}jobid`}
> label={t("jobs.fields.ro_number")}
<JobSearchSelectComponent name={[field.name, "jobid"]}
disabled={context?.jobid} rules={[
notExported={false} {
clm_no required: true,
onChange={(e) => { //message: t("general.validation.required"),
refetch({ jobid: e }); },
}} ]}
/> >
</Form.Item> <JobSearchSelectComponent
</LayoutFormRow> 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>
</Form.Item>
</div>
);
}}
</Form.List>
{/* Lighbox Input amount needs to be hidden */} <Form.Item
<Input shouldUpdate={(prevValues, curValues) =>
className="ipayfield" prevValues.payments?.map((p) => p?.jobid).join() !==
data-ipayname="amount" curValues.payments?.map((p) => p?.jobid).join()
type="hidden" }
value={amount} >
hidden {() => {
/> console.log("Updating the owner info section.");
<Input //If all of the job ids have been fileld in, then query and update the IP field.
className="ipayfield" const { payments } = form.getFieldsValue();
data-ipayname="account" if (
type="hidden" payments?.length > 0 &&
value={data?.jobs_by_pk.ro_number} payments?.filter((p) => p?.jobid).length === payments?.length
hidden ) {
/> console.log("**Calling refetch.");
<Input refetch({ jobids: payments.map((p) => p.jobid) });
className="ipayfield" }
data-ipayname="email" console.log(
type="hidden" "Acc info",
value={data?.jobs_by_pk.owner.ownr_ea} data,
hidden payments && data && data.jobs.length > 0
/> ? data.jobs.map((j) => j.ro_number).join(", ")
{/* Lightbox payment response when it is completed */} : null
<Form.Item name="paymentResponse" hidden> );
<Input type="hidden" value={amount} /> 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
}
hidden
/>
<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
}
hidden
/>
</>
);
}}
</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) => {
return 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)}
hidden
/>
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
loading={queryLoading || loading}
disabled={!(totalAmountToCharge > 0)}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
</Space>
);
}}
</Form.Item> </Form.Item>
<LayoutFormRow grow> {/* Lightbox payment response when it is completed */}
<Form.Item <Form.Item name="paymentResponse" hidden>
label="Amount" <Input type="hidden" />
name="amount" </Form.Item>
rules={[
{
required: true,
// message: t("general.validation.required"),
},
]}
>
<InputNumber />
</Form.Item>
<Row justify="space-around">
<Button
type="primary"
// data-ipayname="submit"
className="ipayfield"
disabled={!amount || !jobid}
onClick={handleIntelliPayCharge}
>
{t("job_payments.buttons.proceedtopayment")}
</Button>
{context?.balance && (
<DataLabel
valueStyle={{
color: context?.balance.getAmount() !== 0 ? "red" : "green",
}}
label={t("payments.labels.balance")}
>
{context?.balance.toFormat()}
</DataLabel>
)}
</Row>
</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,15 +165,17 @@ 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")}>
<InputNumber <Space>
onChange={setRefundAmount} <InputNumber
max={max_refundable_amount} onChange={setRefundAmount}
min={0} max={max_refundable_amount}
/> min={0}
/>
<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,16 +28,14 @@ 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
}
} }
} }
`; `;

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": "",