Created a webhook to allow the transaction to be completed

This commit is contained in:
Tiago Ribeiro
2024-05-15 00:25:44 +01:00
parent 70716b3483
commit 7af96ecccc
6 changed files with 378 additions and 180 deletions

View File

@@ -1,38 +1,73 @@
import {PaymentIntention} from "@/interfaces/paymob";
import {DurationUnit} from "@/interfaces/paypal"; import {DurationUnit} from "@/interfaces/paypal";
import {User} from "@/interfaces/user"; import {User} from "@/interfaces/user";
import axios from "axios"; import axios from "axios";
import {useState} from "react"; import {useState} from "react";
import Button from "./Low/Button"; import Button from "./Low/Button";
import Input from "./Low/Input";
import Modal from "./Modal";
interface Props { interface Props {
user: User; user: User;
currency: string; currency: string;
price: number; price: number;
title: string; packageID: string;
description: string; setIsPaymentLoading: (v: boolean) => void;
paymentID: string;
duration: number; duration: number;
duration_unit: DurationUnit; duration_unit: DurationUnit;
setIsLoading: (isLoading: boolean) => void;
onSuccess: (duration: number, duration_unit: DurationUnit) => void; onSuccess: (duration: number, duration_unit: DurationUnit) => void;
} }
export default function PaymobPayment({ export default function PaymobPayment({user, price, packageID, setIsPaymentLoading, currency, duration, duration_unit, onSuccess}: Props) {
user, const [isLoading, setIsLoading] = useState(false);
price, const [isModalOpen, setIsModalOpen] = useState(false);
currency,
title, const [firstName, setFirstName] = useState(user.name.split(" ")[0]);
description, const [lastName, setLastName] = useState([...user.name.split(" ")].pop());
paymentID, const [street, setStreet] = useState("");
duration, const [apartment, setApartment] = useState("");
duration_unit, const [building, setBuilding] = useState("");
setIsLoading, const [state, setState] = useState("");
onSuccess, const [floor, setFloor] = useState("");
}: Props) {
const [iframeURL, setIFrameURL] = useState<string>();
const handleCardPayment = async () => { const handleCardPayment = async () => {
try { try {
setIsPaymentLoading(true);
const paymentIntention: PaymentIntention = {
amount: price * 1000,
currency: "OMR",
items: [],
payment_methods: [1540],
customer: {
email: user.email,
first_name: user.name.split(" ")[0],
last_name: [...user.name.split(" ")].pop() || "N/A",
extras: {
re: user.id,
},
},
billing_data: {
apartment: apartment || "N/A",
building: building || "N/A",
country: user.demographicInformation?.country || "N/A",
email: user.email,
first_name: user.name.split(" ")[0],
last_name: [...user.name.split(" ")].pop() || "N/A",
floor: floor || "N/A",
phone_number: user.demographicInformation?.phone || "N/A",
state: state || "N/A",
street: street || "N/A",
},
extras: {
userID: user.id,
packageID: packageID,
},
};
const response = await axios.post<{iframeURL: string}>(`/api/paymob`, paymentIntention);
window.open(response.data.iframeURL, "_blank", "noopener,noreferrer");
} catch (error) { } catch (error) {
console.error("Error starting card payment process:", error); console.error("Error starting card payment process:", error);
} }
@@ -40,8 +75,29 @@ export default function PaymobPayment({
return ( return (
<> <>
<Button onClick={handleCardPayment}>Pay</Button> <Modal isOpen={isModalOpen} title="Billing Data" onClose={() => setIsModalOpen(false)}>
{iframeURL} <div className="flex flex-col gap-4 mt-4">
<div className="grid grid-cols-2 gap-4">
<Input label="First Name" value={firstName} onChange={setFirstName} type="text" name="firstName" />
<Input label="Last Name" value={lastName} onChange={setLastName} type="text" name="lastName" />
</div>
<div className="grid grid-cols-3 -md:grid-cols-1 gap-4">
<Input label="State" value={state} onChange={setState} type="text" name="state" />
<Input label="Street" value={street} onChange={setStreet} type="text" name="street" />
<Input label="Building" value={building} onChange={setBuilding} type="text" name="building" />
</div>
<div className="grid grid-cols-2 gap-4">
<Input label="Floor" value={floor} onChange={setFloor} type="text" name="floor" />
<Input label="Apartment" value={apartment} onChange={setApartment} type="text" name="apartment" />
</div>
<Button className="w-full max-w-[200px] self-end mt-4" disabled={!firstName || !lastName} onClick={handleCardPayment}>
Complete Payment
</Button>
</div>
</Modal>
<Button isLoading={isLoading} onClick={() => setIsModalOpen(true)}>
Select
</Button>
</> </>
); );
} }

118
src/interfaces/paymob.ts Normal file
View File

@@ -0,0 +1,118 @@
export interface PaymentIntention {
amount: number;
currency: string;
payment_methods: number[];
items: any[];
billing_data: BillingData;
customer: Customer;
extras: IntentionExtras;
}
interface BillingData {
apartment: string;
first_name: string;
last_name: string;
street: string;
building: string;
phone_number: string;
country: string;
email: string;
floor: string;
state: string;
}
interface Customer {
first_name: string;
last_name: string;
email: string;
extras: IntentionExtras;
}
type IntentionExtras = {[key: string]: string};
export interface IntentionResult {
payment_keys: PaymentKeysItem[];
id: string;
intention_detail: IntentionDetail;
client_secret: string;
payment_methods: PaymentMethodsItem[];
special_reference: null;
extras: Extras;
confirmed: boolean;
status: string;
created: string;
card_detail: null;
object: string;
}
interface PaymentKeysItem {
integration: number;
key: string;
gateway_type: string;
iframe_id: null;
}
interface IntentionDetail {
amount: number;
items: ItemsItem[];
currency: string;
}
interface ItemsItem {
name: string;
amount: number;
description: string;
quantity: number;
}
interface PaymentMethodsItem {
integration_id: number;
alias: null;
name: null;
method_type: string;
currency: string;
live: boolean;
use_cvc_with_moto: boolean;
}
interface Extras {
creation_extras: IntentionExtras;
confirmation_extras: null;
}
export interface TransactionResult {
paymob_request_id: null;
intention: IntentionResult;
hmac: string;
transaction: Transaction;
}
interface Transaction {
amount_cents: number;
created_at: string;
currency: string;
error_occured: boolean;
has_parent_transaction: boolean;
id: number;
integration_id: number;
is_3d_secure: boolean;
is_auth: boolean;
is_capture: boolean;
is_refunded: boolean;
is_standalone_payment: boolean;
is_voided: boolean;
order: Order;
owner: number;
pending: boolean;
source_data: Source_data;
success: boolean;
receipt: string;
}
interface Order {
id: number;
}
interface Source_data {
pan: string;
sub_type: string;
type: string;
}

View File

@@ -107,13 +107,6 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
To add to your use of EnCoach, please purchase one of the time packages available below: To add to your use of EnCoach, please purchase one of the time packages available below:
</span> </span>
<div className="flex w-full flex-wrap justify-center gap-8"> <div className="flex w-full flex-wrap justify-center gap-8">
<PayPalScriptProvider
options={{
clientId: clientID,
currency: "USD",
intent: "capture",
commit: true,
}}>
{packages.map((p) => ( {packages.map((p) => (
<div key={p.id} className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}> <div key={p.id} className={clsx("flex flex-col items-start gap-6 rounded-xl bg-white p-4")}>
<div className="mb-2 flex flex-col items-start"> <div className="mb-2 flex flex-col items-start">
@@ -165,10 +158,8 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
<PaymobPayment <PaymobPayment
key={clientID} key={clientID}
user={user} user={user}
description="Description" setIsPaymentLoading={setIsLoading}
paymentID="123" packageID={p.id}
title="Title"
setIsLoading={setIsLoading}
onSuccess={() => { onSuccess={() => {
setTimeout(reload, 500); setTimeout(reload, 500);
}} }}
@@ -188,7 +179,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
</div> </div>
</div> </div>
))} ))}
</PayPalScriptProvider> z
</div> </div>
</div> </div>
)} )}

View File

@@ -1,101 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, setDoc, doc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
interface BillingData {
apartment: string;
email: string;
floor: string;
first_name: string;
street: string;
building: string;
phone_number: string;
shipping_method: string;
postal_code: string;
city: string;
country: string;
last_name: string;
state: string;
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await getDocs(collection(db, "payments"));
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const body = req.body as Payment;
const shortUID = new ShortUniqueId();
await setDoc(doc(db, "payments", shortUID.randomUUID(8)), body);
res.status(200).json({ok: true});
}
const authenticatePaymob = async () => {
const response = await axios.post<{token: string}>(
"https://oman.paymob.com/api/auth/tokens",
{
api_key: process.env.PAYMOB_API_KEY,
},
{headers: {Authorization: `Bearer ${process.env.PAYMOB_SECRET_KEY}`}},
);
return response.data.token;
};
const createOrder = async (token: string) => {
const response = await axios.post<{id: number}>(
"https://oman.paymob.com/api/ecommerce/orders",
{auth_token: token, delivery_needed: "false", currency: "OMR", amount_cents: "100", items: []},
{headers: {Authorization: `Bearer ${token}`}},
);
return response.data.id;
};
const createTransactionIFrame = async (token: string, orderID: number, billingData: BillingData) => {
const response = await axios.post<{token: string}>(
"https://oman.paymob.com/api/acceptance/payment_keys",
{
auth_token: token,
amount_cents: "100",
order_id: orderID,
currency: "OMR",
expiration: 3600,
integration_id: 1540,
lock_order_when_paid: "true",
billing_data: billingData,
},
{headers: {Authorization: `Bearer ${token}`}},
);
return response.data.token;
};

View File

@@ -0,0 +1,52 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, setDoc, doc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
import {IntentionResult, PaymentIntention} from "@/interfaces/paymob";
const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await getDocs(collection(db, "payments"));
res.status(200).json(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
})),
);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const intention = req.body as PaymentIntention;
const response = await axios.post<IntentionResult>(
"https://oman.paymob.com/v1/intention/",
{...intention, payment_methods: [1540], items: []},
{headers: {Authorization: `Token ${process.env.PAYMOB_SECRET_KEY}`}},
);
const intentionResult = response.data;
res.status(200).json({
iframeURL: `https://oman.paymob.com/unifiedcheckout/?publicKey=${process.env.PAYMOB_PUBLIC_KEY}&clientSecret=${intentionResult.client_secret}`,
});
}

View File

@@ -0,0 +1,82 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
import {getFirestore, collection, getDocs, setDoc, doc, getDoc, query, where} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group, User} from "@/interfaces/user";
import {Package, Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
import {IntentionResult, PaymentIntention, TransactionResult} from "@/interfaces/paymob";
import moment from "moment";
const db = getFirestore(app);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") await post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const transactionResult = req.body as TransactionResult;
const authToken = await authenticatePaymob();
if (!checkTransaction(authToken, transactionResult.transaction.order.id)) return res.status(404).json({ok: false});
if (!transactionResult.transaction.success) return res.status(200).json({ok: false});
const {userID, packageID} = transactionResult.intention.extras.creation_extras;
const userSnapshot = await getDoc(doc(db, "users", userID));
const packageSnapshot = await getDoc(doc(db, "packages", packageID));
if (!userSnapshot.exists() || !packageSnapshot.exists()) return res.status(404).json({ok: false});
const user = {...userSnapshot.data(), id: userSnapshot.id} as User;
const pack = {...packageSnapshot.data(), id: packageSnapshot.id} as Package;
const subscriptionExpirationDate = user.subscriptionExpirationDate;
if (!subscriptionExpirationDate) return res.status(200).json({ok: false});
const initialDate = moment(subscriptionExpirationDate).isAfter(moment()) ? moment(subscriptionExpirationDate) : moment();
const updatedSubscriptionExpirationDate = moment(initialDate).add(pack.duration, pack.duration_unit).toISOString();
await setDoc(userSnapshot.ref, {subscriptionExpirationDate: updatedSubscriptionExpirationDate}, {merge: true});
if (user.type === "corporate") {
const groupsSnapshot = await getDocs(query(collection(db, "groups"), where("admin", "==", user.id)));
const groups = groupsSnapshot.docs.map((g) => ({...g.data(), id: g.id})) as Group[];
const participants = (await Promise.all(
groups.flatMap((x) => x.participants).map(async (x) => ({...(await getDoc(doc(db, "users", x))).data(), id: x})),
)) as User[];
const sameExpiryDateParticipants = participants.filter((x) => x.subscriptionExpirationDate === subscriptionExpirationDate);
for (const participant of sameExpiryDateParticipants) {
await setDoc(doc(db, "users", participant.id), {subscriptionExpirationDate: updatedSubscriptionExpirationDate}, {merge: true});
}
}
res.status(200).json({
ok: true,
});
}
const authenticatePaymob = async () => {
const response = await axios.post<{token: string}>(
"https://oman.paymob.com/api/auth/tokens",
{
api_key: process.env.PAYMOB_API_KEY,
},
{headers: {Authorization: `Bearer ${process.env.PAYMOB_SECRET_KEY}`}},
);
return response.data.token;
};
const checkTransaction = async (token: string, orderID: number) => {
const response = await axios.post("https://oman.paymob.com/api/ecommerce/orders/transaction_inquiry", {auth_token: token, order_id: orderID});
return response.status === 200;
};