added refund and sms feature

This commit is contained in:
swtmply
2023-03-17 01:05:51 +08:00
parent cf017fb80b
commit fa05d0b401
10 changed files with 231 additions and 71 deletions

View File

@@ -1,49 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { setEmailOptions } from "../../redux/email/email.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
});
function Test({ bodyshop, setEmailOptions }) {
return (
<div>
<button
onClick={() => {
setEmailOptions({
messageOptions: {
to: ["patrickwf@gmail.com"],
replyTo: bodyshop.email,
},
template: {
name: TemplateList().parts_order.key,
variables: {
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
},
},
});
}}
>
send email
</button>
<button
onClick={() => {
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
}}
>
Log an ImEX Event.
</button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(Test);

View File

@@ -3,29 +3,27 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import CardPaymentModalContainer from "../card-payment-modal/card-payment-modal.container.";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setCardPaymentContext: (context) => setRefundPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })), dispatch(setModalContext({ context: context, modal: "refund_payment" })),
}); });
function Test({ setCardPaymentContext }) { function Test({ setRefundPaymentContext, refundPaymentModal }) {
console.log("refundPaymentModal", refundPaymentModal);
return ( return (
<div> <div>
<CardPaymentModalContainer />
<Button <Button
onClick={() => onClick={() =>
setCardPaymentContext({ setRefundPaymentContext({
context: {}, context: {},
}) })
} }
> >
Open Modal Open Modal
</Button> </Button>
{/* <IntellipayTestPage /> */}
</div> </div>
); );
} }

View File

@@ -21,7 +21,7 @@ function CardPaymentModalContainer({
toggleModalVisible, toggleModalVisible,
bodyshop, bodyshop,
}) { }) {
const { context, visible, actions } = cardPaymentModal; const { context, visible } = cardPaymentModal;
const handleCancel = () => { const handleCancel = () => {
toggleModalVisible(); toggleModalVisible();

View File

@@ -15,6 +15,12 @@ import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component"; import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
import {
setMessage,
openChatByPhone,
} from "../../redux/messaging/messaging.actions";
import { parsePhoneNumber } from "libphonenumber-js";
import axios from "axios";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -26,12 +32,16 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "payment" })), dispatch(setModalContext({ context: context, modal: "payment" })),
setCardPaymentContext: (context) => setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })), dispatch(setModalContext({ context: context, modal: "cardPayment" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
}); });
export function JobPayments({ export function JobPayments({
job, job,
jobRO, jobRO,
bodyshop, bodyshop,
setMessage,
openChatByPhone,
setPaymentContext, setPaymentContext,
setCardPaymentContext, setCardPaymentContext,
refetch, refetch,
@@ -41,6 +51,8 @@ export function JobPayments({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {}, filteredInfo: {},
}); });
const [generatingURL, setGeneratingtURL] = useState(false);
const columns = [ const columns = [
{ {
title: t("payments.fields.date"), title: t("payments.fields.date"),
@@ -153,6 +165,36 @@ export function JobPayments({
title={t("payments.labels.title")} title={t("payments.labels.title")}
extra={ extra={
<Space wrap> <Space wrap>
<Button
disabled={!job.converted}
loading={generatingURL}
onClick={async () => {
const p = parsePhoneNumber(job.ownr_ph1, "CA");
setGeneratingtURL(true);
const response = await axios.post(
"/intellipay/generate_payment_url",
{
amount: balance.getAmount(),
account: job.ro_number,
}
);
setGeneratingtURL(false);
openChatByPhone({
phone_num: p.formatInternational(),
jobid: job.id,
});
setMessage(
t("appointments.labels.smspaymentreminder", {
shopname: bodyshop.shopname,
amount: balance.toFormat(),
payment_link: response.data.shorUrl,
})
);
}}
>
{t("menus.header.paymentremindersms")}
</Button>
<Button <Button
disabled={!job.converted} disabled={!job.converted}
onClick={() => onClick={() =>
@@ -172,7 +214,7 @@ export function JobPayments({
}) })
} }
> >
Card Payment {t("menus.header.entercardpayment")}
</Button> </Button>
<DataLabel <DataLabel
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }} valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}

View File

@@ -224,7 +224,6 @@ export function JobsDetailHeaderActions({
> >
{t("menus.header.enterpayment")} {t("menus.header.enterpayment")}
</Menu.Item> </Menu.Item>
{/* TODO: Add Card payment */}
<Menu.Item <Menu.Item
key="entercardpayments" key="entercardpayments"
disabled={!job.converted} disabled={!job.converted}

View File

@@ -1,10 +1,22 @@
import React from "react"; import React, { useState } from "react";
import { useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID } from "../../graphql/payment_response.queries"; import {
import { Descriptions } from "antd"; GET_REFUNDABLE_AMOUNT_BY_JOBID,
INSERT_PAYMENT_RESPONSE,
QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID,
} from "../../graphql/payment_response.queries";
import { Button, Descriptions, InputNumber, Modal } from "antd";
import moment from "moment"; import moment from "moment";
import axios from "axios";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
const { confirm } = Modal;
const PaymentExpandedRowComponent = ({ record }) => { const PaymentExpandedRowComponent = ({ record }) => {
const [refundAmount, setRefundAmount] = useState(0);
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
const { loading, error, data } = useQuery( const { loading, error, data } = useQuery(
QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID, QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID,
{ {
@@ -16,12 +28,79 @@ const PaymentExpandedRowComponent = ({ record }) => {
} }
); );
const { data: refundable_amount } = useQuery(GET_REFUNDABLE_AMOUNT_BY_JOBID, {
variables: {
jobid: record.jobid,
},
});
const insertPayments = async (payment_response, refund_response) => {
await insertPayment({
variables: {
paymentInput: {
amount: -refund_response.data.amount,
transactionid: payment_response.response.receiptelements.transid,
payer: record.payer,
type: "Refund",
jobid: payment_response.jobid,
date: moment(Date.now()),
},
},
update(cache, { data }) {
cache.modify({
id: cache.identify({
id: payment_response.jobid,
__typename: "jobs",
}),
fields: {
payments(payments) {
return [...data.insert_payments.returning, ...payments];
},
},
});
},
});
await insertPaymentResponse({
variables: {
paymentResponse: {
amount: -refund_response.data.amount,
bodyshopid: payment_response.bodyshopid,
paymentid: payment_response.paymentid,
jobid: payment_response.jobid,
declinereason: "Refund",
ext_paymentid: payment_response.ext_paymentid,
successful: true,
response: refund_response.data,
},
},
});
};
const showConfirm = (payment_response) => {
confirm({
title: "Do you want to refund payment?",
content:
"The payment will be refunded. Click OK to confirm and Cancel to dismiss.",
async onOk() {
const refundResponse = await axios.post("/intellipay/payment_refund", {
amount: refundAmount,
paymentid: payment_response.ext_paymentid,
});
insertPayments(payment_response, refundResponse);
},
onCancel() {},
});
};
if (loading) return null; if (loading) return null;
if (error) return <p>Error loading data. Please Reload</p>; if (error) return <p>Error loading data. Please Reload</p>;
const payment_response = data.payment_response[0]; const payment_response = data.payment_response[0];
console.log("Record", record); const max_refundable_amount =
refundable_amount.payment_response_aggregate.aggregate.sum.amount;
return ( return (
<div> <div>
@@ -32,7 +111,7 @@ const PaymentExpandedRowComponent = ({ record }) => {
> >
<Descriptions.Item label="Payer">{record.payer}</Descriptions.Item> <Descriptions.Item label="Payer">{record.payer}</Descriptions.Item>
<Descriptions.Item label="Payer Name"> <Descriptions.Item label="Payer Name">
{payment_response.response.nameOnCard} {payment_response?.response?.nameOnCard ?? ""}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Amount">{record.amount}</Descriptions.Item> <Descriptions.Item label="Amount">{record.amount}</Descriptions.Item>
<Descriptions.Item label="Date of Payment"> <Descriptions.Item label="Date of Payment">
@@ -42,11 +121,24 @@ const PaymentExpandedRowComponent = ({ record }) => {
{record.transactionid} {record.transactionid}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Payment Reference ID"> <Descriptions.Item label="Payment Reference ID">
{payment_response.response.paymentreferenceid} {payment_response?.response?.paymentreferenceid ?? ""}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Payment Type"> <Descriptions.Item label="Payment Type">
{record.type} {record.type}
</Descriptions.Item> </Descriptions.Item>
{payment_response && (
<Descriptions.Item label="Refund Amount">
<InputNumber
onChange={setRefundAmount}
max={max_refundable_amount}
min={0}
/>
<Button onClick={() => showConfirm(payment_response)}>
Refund Payment
</Button>
</Descriptions.Item>
)}
</Descriptions> </Descriptions>
</div> </div>
); );

View File

@@ -16,6 +16,9 @@ export const QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID = gql`
query QUERY_PAYMENT_RESPONSE_BY_PK($paymentid: uuid!) { query QUERY_PAYMENT_RESPONSE_BY_PK($paymentid: uuid!) {
payment_response(where: { paymentid: { _eq: $paymentid } }) { payment_response(where: { paymentid: { _eq: $paymentid } }) {
id id
jobid
bodyshopid
paymentid
amount amount
declinereason declinereason
ext_paymentid ext_paymentid
@@ -26,7 +29,7 @@ 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_PK = gql`
query GET_JOB_OWNER($jobid: uuid!) { query QUERY_RO_AND_OWNER_BY_JOB_PK($jobid: uuid!) {
jobs_by_pk(id: $jobid) { jobs_by_pk(id: $jobid) {
ro_number ro_number
owner { owner {
@@ -38,3 +41,15 @@ export const QUERY_RO_AND_OWNER_BY_JOB_PK = gql`
} }
} }
`; `;
export const GET_REFUNDABLE_AMOUNT_BY_JOBID = gql`
query GET_REFUNDABLE_AMOUNT_BY_JOBID($jobid: uuid!) {
payment_response_aggregate(where: { jobid: { _eq: $jobid } }) {
aggregate {
sum {
amount
}
}
}
}
`;

View File

@@ -60,6 +60,7 @@
"nodateselected": "No date has been selected.", "nodateselected": "No date has been selected.",
"priorappointments": "Previous Appointments", "priorappointments": "Previous Appointments",
"reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ", "reminder": "This is {{shopname}} reminding you about an appointment on {{date}} at {{time}}. Please let us know if you are not able to make the appointment. We look forward to seeing you soon. ",
"smspaymentreminder": "This is {{shopname}} reminding you about your remaining balance of {{amount}}. To pay for the said balance click the link {{payment_link}}. Thank you very much.",
"scheduledfor": "Scheduled appointment for: ", "scheduledfor": "Scheduled appointment for: ",
"severalerrorsfound": "Several jobs have issues which may prevent accurate smart scheduling. Click to expand.", "severalerrorsfound": "Several jobs have issues which may prevent accurate smart scheduling. Click to expand.",
"smartscheduling": "Smart Scheduling", "smartscheduling": "Smart Scheduling",
@@ -1840,6 +1841,7 @@
"dashboard": "Dashboard", "dashboard": "Dashboard",
"enterbills": "Enter Bills", "enterbills": "Enter Bills",
"enterpayment": "Enter Payments", "enterpayment": "Enter Payments",
"paymentremindersms": "Send Payment Reminder via SMS",
"entercardpayment": "Enter Card Payments", "entercardpayment": "Enter Card Payments",
"entertimeticket": "Enter Time Tickets", "entertimeticket": "Enter Time Tickets",
"export": "Export", "export": "Export",

View File

@@ -232,6 +232,18 @@ app.get(
intellipay.lightbox_credentials intellipay.lightbox_credentials
); );
app.post(
"/intellipay/payment_refund",
fb.validateFirebaseIdToken,
intellipay.payment_refund
);
app.post(
"/intellipay/generate_payment_url",
fb.validateFirebaseIdToken,
intellipay.generate_payment_url
);
var ioevent = require("./server/ioevent/ioevent"); var ioevent = require("./server/ioevent/ioevent");
app.post("/ioevent", ioevent.default); app.post("/ioevent", ioevent.default);
app.post("/newlog", (req, res) => { app.post("/newlog", (req, res) => {

View File

@@ -11,9 +11,7 @@ require("dotenv").config({
), ),
}); });
const url = process.env.NODE_ENV const domain = process.env.NODE_ENV ? "secure" : "test";
? "https://secure.cpteller.com/api/custapi.cfc?method=autoterminal"
: "https://test.cpteller.com/api/custapi.cfc?method=autoterminal";
const getShopCredentials = () => { const getShopCredentials = () => {
// add parametes for the request // add parametes for the request
@@ -44,7 +42,58 @@ exports.lightbox_credentials = async (req, res) => {
? process.env.NODE_ENV ? process.env.NODE_ENV
: "businessattended", : "businessattended",
}), }),
url, url: `https://${domain}.cpteller.com/api/custapi.cfc?method=autoterminal`,
};
const response = await axios(options);
res.send(response.data);
} catch (error) {
console.log(error);
res.json({ error });
}
};
exports.payment_refund = async (req, res) => {
const shopCredentials = getShopCredentials();
try {
const options = {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
data: qs.stringify({
method: "payment_refund",
...shopCredentials,
paymentid: req.body.paymentid,
amount: req.body.amount,
}),
url: `https://${domain}.cpteller.com/api/26/webapi.cfc?method=payment_refund`,
};
const response = await axios(options);
res.send(response.data);
} catch (error) {
console.log(error);
res.json({ error });
}
};
exports.generate_payment_url = async (req, res) => {
const shopCredentials = getShopCredentials();
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,
createshorturl: true,
}),
url: `https://${domain}.cpteller.com/api/custapi.cfc?method=generate_lightbox_url`,
}; };
const response = await axios(options); const response = await axios(options);