added refund and sms feature
This commit is contained in:
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ function CardPaymentModalContainer({
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
}) {
|
||||
const { context, visible, actions } = cardPaymentModal;
|
||||
const { context, visible } = cardPaymentModal;
|
||||
|
||||
const handleCancel = () => {
|
||||
toggleModalVisible();
|
||||
|
||||
@@ -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" }}
|
||||
|
||||
@@ -224,7 +224,6 @@ export function JobsDetailHeaderActions({
|
||||
>
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
{/* TODO: Add Card payment */}
|
||||
<Menu.Item
|
||||
key="entercardpayments"
|
||||
disabled={!job.converted}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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",
|
||||
|
||||
12
server.js
12
server.js
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user