diff --git a/client/src/components/_test/paymentMethod.jsx b/client/src/components/_test/paymentMethod.jsx deleted file mode 100644 index 8e722cc0b..000000000 --- a/client/src/components/_test/paymentMethod.jsx +++ /dev/null @@ -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 ( -
- - -
- ); -} - -export default connect(mapStateToProps, mapDispatchToProps)(Test); diff --git a/client/src/components/_test/test.page.jsx b/client/src/components/_test/test.page.jsx index 524a41a45..13195631b 100644 --- a/client/src/components/_test/test.page.jsx +++ b/client/src/components/_test/test.page.jsx @@ -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 (
- - {/* */}
); } diff --git a/client/src/components/card-payment-modal/card-payment-modal.container..jsx b/client/src/components/card-payment-modal/card-payment-modal.container..jsx index a06692dbb..7ba4ed2ac 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.container..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.container..jsx @@ -21,7 +21,7 @@ function CardPaymentModalContainer({ toggleModalVisible, bodyshop, }) { - const { context, visible, actions } = cardPaymentModal; + const { context, visible } = cardPaymentModal; const handleCancel = () => { toggleModalVisible(); diff --git a/client/src/components/job-payments/job-payments.component.jsx b/client/src/components/job-payments/job-payments.component.jsx index 2b470a36c..196ef0baa 100644 --- a/client/src/components/job-payments/job-payments.component.jsx +++ b/client/src/components/job-payments/job-payments.component.jsx @@ -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={ + {t("menus.header.enterpayment")} - {/* TODO: Add Card payment */} { + 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

Error loading data. Please Reload

; const payment_response = data.payment_response[0]; - console.log("Record", record); + const max_refundable_amount = + refundable_amount.payment_response_aggregate.aggregate.sum.amount; return (
@@ -32,7 +111,7 @@ const PaymentExpandedRowComponent = ({ record }) => { > {record.payer} - {payment_response.response.nameOnCard} + {payment_response?.response?.nameOnCard ?? ""} {record.amount} @@ -42,11 +121,24 @@ const PaymentExpandedRowComponent = ({ record }) => { {record.transactionid} - {payment_response.response.paymentreferenceid} + {payment_response?.response?.paymentreferenceid ?? ""} {record.type} + {payment_response && ( + + + + + + )}
); diff --git a/client/src/graphql/payment_response.queries.js b/client/src/graphql/payment_response.queries.js index 5a7934824..8722e6cd8 100644 --- a/client/src/graphql/payment_response.queries.js +++ b/client/src/graphql/payment_response.queries.js @@ -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 + } + } + } + } +`; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index c5eba4867..691918729 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -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", diff --git a/server.js b/server.js index 12a90f3c8..531c7813f 100644 --- a/server.js +++ b/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) => { diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 6196dc729..c9646088b 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -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);