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 { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import CardPaymentModalContainer from "../card-payment-modal/card-payment-modal.container.";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
setRefundPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "refund_payment" })),
});
function Test({ setCardPaymentContext }) {
function Test({ setRefundPaymentContext, refundPaymentModal }) {
console.log("refundPaymentModal", refundPaymentModal);
return (
<div>
<CardPaymentModalContainer />
<Button
onClick={() =>
setCardPaymentContext({
setRefundPaymentContext({
context: {},
})
}
>
Open Modal
</Button>
{/* <IntellipayTestPage /> */}
</div>
);
}

View File

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

View File

@@ -15,6 +15,12 @@ import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.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({
bodyshop: selectBodyshop,
@@ -26,12 +32,16 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "payment" })),
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
});
export function JobPayments({
job,
jobRO,
bodyshop,
setMessage,
openChatByPhone,
setPaymentContext,
setCardPaymentContext,
refetch,
@@ -41,6 +51,8 @@ export function JobPayments({
sortedInfo: {},
filteredInfo: {},
});
const [generatingURL, setGeneratingtURL] = useState(false);
const columns = [
{
title: t("payments.fields.date"),
@@ -153,6 +165,36 @@ export function JobPayments({
title={t("payments.labels.title")}
extra={
<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
disabled={!job.converted}
onClick={() =>
@@ -172,7 +214,7 @@ export function JobPayments({
})
}
>
Card Payment
{t("menus.header.entercardpayment")}
</Button>
<DataLabel
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}

View File

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

View File

@@ -1,10 +1,22 @@
import React from "react";
import { useQuery } from "@apollo/client";
import { QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID } from "../../graphql/payment_response.queries";
import { Descriptions } from "antd";
import React, { useState } from "react";
import { useMutation, useQuery } from "@apollo/client";
import {
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 axios from "axios";
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
const { confirm } = Modal;
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(
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 (error) return <p>Error loading data. Please Reload</p>;
const payment_response = data.payment_response[0];
console.log("Record", record);
const max_refundable_amount =
refundable_amount.payment_response_aggregate.aggregate.sum.amount;
return (
<div>
@@ -32,7 +111,7 @@ const PaymentExpandedRowComponent = ({ record }) => {
>
<Descriptions.Item label="Payer">{record.payer}</Descriptions.Item>
<Descriptions.Item label="Payer Name">
{payment_response.response.nameOnCard}
{payment_response?.response?.nameOnCard ?? ""}
</Descriptions.Item>
<Descriptions.Item label="Amount">{record.amount}</Descriptions.Item>
<Descriptions.Item label="Date of Payment">
@@ -42,11 +121,24 @@ const PaymentExpandedRowComponent = ({ record }) => {
{record.transactionid}
</Descriptions.Item>
<Descriptions.Item label="Payment Reference ID">
{payment_response.response.paymentreferenceid}
{payment_response?.response?.paymentreferenceid ?? ""}
</Descriptions.Item>
<Descriptions.Item label="Payment Type">
{record.type}
</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>
</div>
);

View File

@@ -16,6 +16,9 @@ export const QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID = gql`
query QUERY_PAYMENT_RESPONSE_BY_PK($paymentid: uuid!) {
payment_response(where: { paymentid: { _eq: $paymentid } }) {
id
jobid
bodyshopid
paymentid
amount
declinereason
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`
query GET_JOB_OWNER($jobid: uuid!) {
query QUERY_RO_AND_OWNER_BY_JOB_PK($jobid: uuid!) {
jobs_by_pk(id: $jobid) {
ro_number
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.",
"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. ",
"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: ",
"severalerrorsfound": "Several jobs have issues which may prevent accurate smart scheduling. Click to expand.",
"smartscheduling": "Smart Scheduling",
@@ -1840,6 +1841,7 @@
"dashboard": "Dashboard",
"enterbills": "Enter Bills",
"enterpayment": "Enter Payments",
"paymentremindersms": "Send Payment Reminder via SMS",
"entercardpayment": "Enter Card Payments",
"entertimeticket": "Enter Time Tickets",
"export": "Export",

View File

@@ -232,6 +232,18 @@ app.get(
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");
app.post("/ioevent", ioevent.default);
app.post("/newlog", (req, res) => {

View File

@@ -11,9 +11,7 @@ require("dotenv").config({
),
});
const url = process.env.NODE_ENV
? "https://secure.cpteller.com/api/custapi.cfc?method=autoterminal"
: "https://test.cpteller.com/api/custapi.cfc?method=autoterminal";
const domain = process.env.NODE_ENV ? "secure" : "test";
const getShopCredentials = () => {
// add parametes for the request
@@ -44,7 +42,58 @@ exports.lightbox_credentials = async (req, res) => {
? process.env.NODE_ENV
: "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);