From 8b2459c304a5bc03e5be3b3f377046bb60860026 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 8 May 2024 15:46:24 +0100 Subject: [PATCH 01/12] ENCOA-37: Added the ability for users to download a list of the shown users --- src/hooks/useListSearch.tsx | 66 ++++++++++++---------------- src/pages/(admin)/Lists/UserList.tsx | 61 ++++++++++++------------- src/resources/user.ts | 15 ++++++- src/utils/users.ts | 36 +++++++++++++++ 4 files changed, 105 insertions(+), 73 deletions(-) create mode 100644 src/utils/users.ts diff --git a/src/hooks/useListSearch.tsx b/src/hooks/useListSearch.tsx index a559b400..ff3ddb85 100644 --- a/src/hooks/useListSearch.tsx +++ b/src/hooks/useListSearch.tsx @@ -1,4 +1,4 @@ -import {useState, useMemo} from 'react'; +import {useState, useMemo} from "react"; import Input from "@/components/Low/Input"; /*fields example = [ @@ -6,43 +6,33 @@ import Input from "@/components/Low/Input"; ['companyInformation', 'companyInformation', 'name'] ]*/ - const getFieldValue = (fields: string[], data: any): string => { - if(fields.length === 0) return data; - const [key, ...otherFields] = fields; + if (fields.length === 0) return data; + const [key, ...otherFields] = fields; - if(data[key]) return getFieldValue(otherFields, data[key]); - return data; + if (data[key]) return getFieldValue(otherFields, data[key]); + return data; +}; + +export function useListSearch(fields: string[][], rows: T[]) { + const [text, setText] = useState(""); + + const renderSearch = () => ; + + const updatedRows = useMemo(() => { + const searchText = text.toLowerCase(); + return rows.filter((row) => { + return fields.some((fieldsKeys) => { + const value = getFieldValue(fieldsKeys, row); + if (typeof value === "string") { + return value.toLowerCase().includes(searchText); + } + }); + }); + }, [fields, rows, text]); + + return { + rows: updatedRows, + renderSearch, + }; } - -export const useListSearch = (fields: string[][], rows: any[]) => { - const [text, setText] = useState(''); - - const renderSearch = () => ( - - ) - - const updatedRows = useMemo(() => { - const searchText = text.toLowerCase(); - return rows.filter((row) => { - return fields.some((fieldsKeys) => { - const value = getFieldValue(fieldsKeys, row); - if(typeof value === 'string') { - return value.toLowerCase().includes(searchText); - } - }) - }) - }, [fields, rows, text]) - - return { - rows: updatedRows, - renderSearch, - } -} \ No newline at end of file diff --git a/src/pages/(admin)/Lists/UserList.tsx b/src/pages/(admin)/Lists/UserList.tsx index c163187d..9ade7c31 100644 --- a/src/pages/(admin)/Lists/UserList.tsx +++ b/src/pages/(admin)/Lists/UserList.tsx @@ -16,49 +16,25 @@ import {countries, TCountries} from "countries-list"; import countryCodes from "country-codes-list"; import Modal from "@/components/Modal"; import UserCard from "@/components/UserCard"; -import {isAgentUser, USER_TYPE_LABELS} from "@/resources/user"; +import {getUserCompanyName, isAgentUser, USER_TYPE_LABELS} from "@/resources/user"; import useFilterStore from "@/stores/listFilterStore"; import {useRouter} from "next/router"; import {isCorporateUser} from "@/resources/user"; import {useListSearch} from "@/hooks/useListSearch"; import {getUserCorporate} from "@/utils/groups"; import {asyncSorter} from "@/utils"; +import {exportListToExcel, UserListRow} from "@/utils/users"; const columnHelper = createColumnHelper(); const searchFields = [["name"], ["email"], ["corporateInformation", "companyInformation", "name"]]; -const getCompanyName = async (user: User) => { - if (isCorporateUser(user)) { - return user.corporateInformation?.companyInformation?.name; - } - - if (isAgentUser(user)) { - return user.agentInformation.companyName; - } - - if (user.type === "teacher" || user.type === "student") { - const userCorporate = await getUserCorporate(user.id); - return userCorporate?.corporateInformation?.companyInformation.name || ""; - } - - return ""; -}; - -const CompanyNameCell = ({users, user, groups}: {user: User, users: User[], groups: Group[]}) => { +const CompanyNameCell = ({users, user, groups}: {user: User; users: User[]; groups: Group[]}) => { const [companyName, setCompanyName] = useState(""); const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - if (isCorporateUser(user)) return setCompanyName(user.corporateInformation?.companyInformation?.name || user.name) - if (isAgentUser(user)) return setCompanyName(user.agentInformation?.companyName || user.name) - - const belongingGroups = groups.filter((x) => x.participants.includes(user.id)) - const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x)) - - if (belongingGroupsAdmins.length === 0) return setCompanyName("") - - const admin = (belongingGroupsAdmins[0] as CorporateUser) - setCompanyName(admin.corporateInformation?.companyInformation.name || admin.name) + useEffect(() => { + const name = getUserCompanyName(user, users, groups); + setCompanyName(name); }, [user, users, groups]); return isLoading ? Loading... : <>{companyName}; @@ -501,8 +477,8 @@ export default function UserList({user, filters = []}: {user: User; filters?: (( } if (sorter === "companyName" || sorter === reverseString("companyName")) { - const aCorporateName = await getCompanyName(a); - const bCorporateName = await getCompanyName(b); + const aCorporateName = getUserCompanyName(a, users, groups); + const bCorporateName = getUserCompanyName(b, users, groups); if (!aCorporateName && bCorporateName) return sorter === "companyName" ? -1 : 1; if (aCorporateName && !bCorporateName) return sorter === "companyName" ? 1 : -1; if (!aCorporateName && !bCorporateName) return 0; @@ -513,7 +489,7 @@ export default function UserList({user, filters = []}: {user: User; filters?: (( return a.id.localeCompare(b.id); }; - const {rows: filteredRows, renderSearch} = useListSearch(searchFields, displayUsers); + const {rows: filteredRows, renderSearch} = useListSearch(searchFields, displayUsers); const table = useReactTable({ data: filteredRows, @@ -521,6 +497,18 @@ export default function UserList({user, filters = []}: {user: User; filters?: (( getCoreRowModel: getCoreRowModel(), }); + const downloadExcel = () => { + const csv = exportListToExcel(filteredRows, users, groups); + + const element = document.createElement("a"); + const file = new Blob([csv], {type: "text/plain"}); + element.href = URL.createObjectURL(file); + element.download = "users.xlsx"; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }; + return (
setSelectedUser(undefined)}> @@ -600,7 +588,12 @@ export default function UserList({user, filters = []}: {user: User; filters?: ((
- {renderSearch()} +
+ {renderSearch()} + +
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/src/resources/user.ts b/src/resources/user.ts index c6a516c1..dfeac0da 100644 --- a/src/resources/user.ts +++ b/src/resources/user.ts @@ -1,4 +1,4 @@ -import {Type, User, CorporateUser, AgentUser} from "@/interfaces/user"; +import {Type, User, CorporateUser, AgentUser, Group} from "@/interfaces/user"; export const USER_TYPE_LABELS: {[key in Type]: string} = { student: "Student", @@ -16,3 +16,16 @@ export function isCorporateUser(user: User): user is CorporateUser { export function isAgentUser(user: User): user is AgentUser { return (user as AgentUser).agentInformation !== undefined; } + +export function getUserCompanyName(user: User, users: User[], groups: Group[]) { + if (isCorporateUser(user)) return user.corporateInformation?.companyInformation?.name || user.name; + if (isAgentUser(user)) return user.agentInformation?.companyName || user.name; + + const belongingGroups = groups.filter((x) => x.participants.includes(user.id)); + const belongingGroupsAdmins = belongingGroups.map((x) => users.find((u) => u.id === x.admin)).filter((x) => !!x && isCorporateUser(x)); + + if (belongingGroupsAdmins.length === 0) return ""; + + const admin = belongingGroupsAdmins[0] as CorporateUser; + return admin.corporateInformation?.companyInformation.name || admin.name; +} diff --git a/src/utils/users.ts b/src/utils/users.ts new file mode 100644 index 00000000..a5918bf7 --- /dev/null +++ b/src/utils/users.ts @@ -0,0 +1,36 @@ +import {Group, User} from "@/interfaces/user"; +import {getUserCompanyName, USER_TYPE_LABELS} from "@/resources/user"; +import {capitalize} from "lodash"; +import moment from "moment"; + +export interface UserListRow { + name: string; + email: string; + type: string; + companyName: string; + expiryDate: string; + verified: string; + country: string; + phone: string; + employmentPosition: string; + gender: string; +} + +export const exportListToExcel = (rowUsers: User[], users: User[], groups: Group[]) => { + const rows: UserListRow[] = rowUsers.map((user) => ({ + name: user.name, + email: user.email, + type: USER_TYPE_LABELS[user.type], + companyName: getUserCompanyName(user, users, groups), + expiryDate: user.subscriptionExpirationDate ? moment(user.subscriptionExpirationDate).format("DD/MM/YYYY") : "Unlimited", + country: user.demographicInformation?.country || "N/A", + phone: user.demographicInformation?.phone || "N/A", + employmentPosition: (user.type === "corporate" ? user.demographicInformation?.position : user.demographicInformation?.employment) || "N/A", + gender: user.demographicInformation?.gender ? capitalize(user.demographicInformation.gender) : "N/A", + verified: user.isVerified?.toString() || "FALSE", + })); + const header = "Name,Email,Type,Company Name,Expiry Date,Country,Phone,Employment/Position,Gender,Verification"; + const rowsString = rows.map((x) => Object.values(x).join(",")).join("\n"); + + return `${header}\n${rowsString}`; +}; From f967282f71d69e111dffb4583042ec6f8db3a959 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 13 May 2024 10:38:05 +0100 Subject: [PATCH 02/12] Started implementing the Paymob integration --- package.json | 3 +- src/components/PaymobPayment.tsx | 47 +++ src/pages/(status)/PaymentDue.tsx | 520 +++++++++++++----------------- src/pages/api/paymob.ts | 101 ++++++ yarn.lock | 29 ++ 5 files changed, 407 insertions(+), 293 deletions(-) create mode 100644 src/components/PaymobPayment.tsx create mode 100644 src/pages/api/paymob.ts diff --git a/package.json b/package.json index 6a4285a8..5e91f501 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "next": "13.1.6", "nodemailer": "^6.9.5", "nodemailer-express-handlebars": "^6.1.0", + "paymob-react": "git+https://github.com/tiago-ecrop/paymob-react-oman.git", "primeicons": "^6.0.1", "primereact": "^9.2.3", "qrcode": "^1.5.3", @@ -98,4 +99,4 @@ "tailwindcss": "^3.2.4", "types/": "paypal/react-paypal-js" } -} +} \ No newline at end of file diff --git a/src/components/PaymobPayment.tsx b/src/components/PaymobPayment.tsx new file mode 100644 index 00000000..fe27543f --- /dev/null +++ b/src/components/PaymobPayment.tsx @@ -0,0 +1,47 @@ +import {DurationUnit} from "@/interfaces/paypal"; +import {User} from "@/interfaces/user"; +import axios from "axios"; +import {useState} from "react"; +import Button from "./Low/Button"; + +interface Props { + user: User; + currency: string; + price: number; + title: string; + description: string; + paymentID: string; + duration: number; + duration_unit: DurationUnit; + setIsLoading: (isLoading: boolean) => void; + onSuccess: (duration: number, duration_unit: DurationUnit) => void; +} + +export default function PaymobPayment({ + user, + price, + currency, + title, + description, + paymentID, + duration, + duration_unit, + setIsLoading, + onSuccess, +}: Props) { + const [iframeURL, setIFrameURL] = useState(); + + const handleCardPayment = async () => { + try { + } catch (error) { + console.error("Error starting card payment process:", error); + } + }; + + return ( + <> + + {iframeURL} + + ); +} diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 5809653b..3cb5b680 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -4,194 +4,147 @@ import PayPalPayment from "@/components/PayPalPayment"; import useGroups from "@/hooks/useGroups"; import usePackages from "@/hooks/usePackages"; import useUsers from "@/hooks/useUsers"; -import { User } from "@/interfaces/user"; +import {User} from "@/interfaces/user"; import clsx from "clsx"; -import { capitalize } from "lodash"; -import { useEffect, useState } from "react"; +import {capitalize} from "lodash"; +import {useEffect, useState} from "react"; import getSymbolFromCurrency from "currency-symbol-map"; import useInvites from "@/hooks/useInvites"; -import { BsArrowRepeat } from "react-icons/bs"; +import {BsArrowRepeat} from "react-icons/bs"; import InviteCard from "@/components/Medium/InviteCard"; -import { useRouter } from "next/router"; -import { PayPalScriptProvider } from "@paypal/react-paypal-js"; -import { usePaypalTracking } from "@/hooks/usePaypalTracking"; -import { ToastContainer } from "react-toastify"; +import {useRouter} from "next/router"; +import {PayPalScriptProvider} from "@paypal/react-paypal-js"; +import {usePaypalTracking} from "@/hooks/usePaypalTracking"; +import {ToastContainer} from "react-toastify"; import useDiscounts from "@/hooks/useDiscounts"; +import PaymobPayment from "@/components/PaymobPayment"; interface Props { - user: User; - hasExpired?: boolean; - clientID: string; - reload: () => void; + user: User; + hasExpired?: boolean; + clientID: string; + reload: () => void; } -export default function PaymentDue({ - user, - hasExpired = false, - clientID, - reload, -}: Props) { - const [isLoading, setIsLoading] = useState(false); - const [appliedDiscount, setAppliedDiscount] = useState(0); +export default function PaymentDue({user, hasExpired = false, clientID, reload}: Props) { + const [isLoading, setIsLoading] = useState(false); + const [appliedDiscount, setAppliedDiscount] = useState(0); - const router = useRouter(); + const router = useRouter(); - const { packages } = usePackages(); - const { discounts } = useDiscounts(); - const { users } = useUsers(); - const { groups } = useGroups(); - const { - invites, - isLoading: isInvitesLoading, - reload: reloadInvites, - } = useInvites({ to: user?.id }); - const trackingId = usePaypalTracking(); + const {packages} = usePackages(); + const {discounts} = useDiscounts(); + const {users} = useUsers(); + const {groups} = useGroups(); + const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id}); + const trackingId = usePaypalTracking(); - useEffect(() => { - const userDiscounts = discounts.filter((x) => - user.email.endsWith(`@${x.domain}`), - ); - if (userDiscounts.length === 0) return; + useEffect(() => { + const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`)); + if (userDiscounts.length === 0) return; - const biggestDiscount = [...userDiscounts] - .sort((a, b) => b.percentage - a.percentage) - .shift(); - if (!biggestDiscount) return; + const biggestDiscount = [...userDiscounts].sort((a, b) => b.percentage - a.percentage).shift(); + if (!biggestDiscount) return; - setAppliedDiscount(biggestDiscount.percentage); - }, [discounts, user]); + setAppliedDiscount(biggestDiscount.percentage); + }, [discounts, user]); - const isIndividual = () => { - if (user?.type === "developer") return true; - if (user?.type !== "student") return false; - const userGroups = groups.filter((g) => g.participants.includes(user?.id)); + const isIndividual = () => { + if (user?.type === "developer") return true; + if (user?.type !== "student") return false; + const userGroups = groups.filter((g) => g.participants.includes(user?.id)); - if (userGroups.length === 0) return true; + if (userGroups.length === 0) return true; - const userGroupsAdminTypes = userGroups - .map((g) => users?.find((u) => u.id === g.admin)?.type) - .filter((t) => !!t); - return userGroupsAdminTypes.every((t) => t !== "corporate"); - }; + const userGroupsAdminTypes = userGroups.map((g) => users?.find((u) => u.id === g.admin)?.type).filter((t) => !!t); + return userGroupsAdminTypes.every((t) => t !== "corporate"); + }; - return ( - <> - - {isLoading && ( -
-
- - - Completing your payment... - -
-
- )} - {user ? ( - - {invites.length > 0 && ( -
-
-
- - Invites - - -
-
- - {invites.map((invite) => ( - { - reloadInvites(); - router.reload(); - }} - /> - ))} - -
- )} + return ( + <> + + {isLoading && ( +
+
+ + Completing your payment... +
+
+ )} + {user ? ( + + {invites.length > 0 && ( +
+
+
+ Invites + +
+
+ + {invites.map((invite) => ( + { + reloadInvites(); + router.reload(); + }} + /> + ))} + +
+ )} -
- {hasExpired && ( - - You do not have time credits for your account type! - - )} - {isIndividual() && ( -
- - To add to your use of EnCoach, please purchase one of the time - packages available below: - -
- - {packages.map((p) => ( -
-
- EnCoach's Logo - - EnCoach - {p.duration}{" "} - {capitalize( - p.duration === 1 - ? p.duration_unit.slice( - 0, - p.duration_unit.length - 1, - ) - : p.duration_unit, - )} - -
-
- {!appliedDiscount && ( - - {p.price} - {getSymbolFromCurrency(p.currency)} - - )} - {appliedDiscount && ( -
- - {p.price} - {getSymbolFromCurrency(p.currency)} - - - {( - p.price - - p.price * (appliedDiscount / 100) - ).toFixed(2)} - {getSymbolFromCurrency(p.currency)} - -
- )} - + {hasExpired && You do not have time credits for your account type!} + {isIndividual() && ( +
+ + To add to your use of EnCoach, please purchase one of the time packages available below: + +
+ + {packages.map((p) => ( +
+
+ EnCoach's Logo + + EnCoach - {p.duration}{" "} + {capitalize( + p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, + )} + +
+
+ {!appliedDiscount && ( + + {p.price} + {getSymbolFromCurrency(p.currency)} + + )} + {appliedDiscount && ( +
+ + {p.price} + {getSymbolFromCurrency(p.currency)} + + + {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} + {getSymbolFromCurrency(p.currency)} + +
+ )} + {/* -
-
- This includes: -
    -
  • - Train your abilities for the IELTS exam
  • -
  • - - Gain insights into your weaknesses and strengths -
  • -
  • - - Allow yourself to correctly prepare for the exam -
  • -
-
-
- ))} -
-
-
- )} - {!isIndividual() && - user.type === "corporate" && - user?.corporateInformation.payment && ( -
- - To add to your use of EnCoach and that of your students and - teachers, please pay your designated package below: - -
-
- EnCoach's Logo - - EnCoach - {user.corporateInformation?.monthlyDuration}{" "} - Months - -
-
- - {user.corporateInformation.payment.value} - {getSymbolFromCurrency( - user.corporateInformation.payment.currency, - )} - - { - setIsLoading(false); - setTimeout(reload, 500); - }} - loadScript - trackingId={trackingId} - /> -
-
- This includes: -
    -
  • - - Allow a total of{" "} - { - user.corporateInformation.companyInformation - .userAmount - }{" "} - students and teachers to use EnCoach -
  • -
  • - Train their abilities for the IELTS exam
  • -
  • - - Gain insights into your students' weaknesses - and strengths -
  • -
  • - Allow them to correctly prepare for the exam
  • -
-
-
-
- )} - {!isIndividual() && user.type !== "corporate" && ( -
- - You are not the person in charge of your time credits, please - contact your administrator about this situation. - - - If you believe this to be a mistake, please contact the - platform's administration, thank you for your patience. - -
- )} - {!isIndividual() && - user.type === "corporate" && - !user.corporateInformation.payment && ( -
- - An admin nor your agent have yet set the price intended to - your requirements in terms of the amount of users you desire - and your expected monthly duration. - - - Please try again later or contact your agent or an admin, - thank you for your patience. - -
- )} -
- - ) : ( -
- )} - - ); + /> */} + { + setTimeout(reload, 500); + }} + currency={p.currency} + duration={p.duration} + duration_unit={p.duration_unit} + price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} + /> +
+
+ This includes: +
    +
  • - Train your abilities for the IELTS exam
  • +
  • - Gain insights into your weaknesses and strengths
  • +
  • - Allow yourself to correctly prepare for the exam
  • +
+
+
+ ))} +
+
+
+ )} + {!isIndividual() && user.type === "corporate" && user?.corporateInformation.payment && ( +
+ + To add to your use of EnCoach and that of your students and teachers, please pay your designated package below: + +
+
+ EnCoach's Logo + EnCoach - {user.corporateInformation?.monthlyDuration} Months +
+
+ + {user.corporateInformation.payment.value} + {getSymbolFromCurrency(user.corporateInformation.payment.currency)} + + { + setIsLoading(false); + setTimeout(reload, 500); + }} + loadScript + trackingId={trackingId} + /> +
+
+ This includes: +
    +
  • + - Allow a total of {user.corporateInformation.companyInformation.userAmount} students and teachers to + use EnCoach +
  • +
  • - Train their abilities for the IELTS exam
  • +
  • - Gain insights into your students' weaknesses and strengths
  • +
  • - Allow them to correctly prepare for the exam
  • +
+
+
+
+ )} + {!isIndividual() && user.type !== "corporate" && ( +
+ + You are not the person in charge of your time credits, please contact your administrator about this situation. + + + If you believe this to be a mistake, please contact the platform's administration, thank you for your + patience. + +
+ )} + {!isIndividual() && user.type === "corporate" && !user.corporateInformation.payment && ( +
+ + An admin nor your agent have yet set the price intended to your requirements in terms of the amount of users you + desire and your expected monthly duration. + + + Please try again later or contact your agent or an admin, thank you for your patience. + +
+ )} +
+
+ ) : ( +
+ )} + + ); } diff --git a/src/pages/api/paymob.ts b/src/pages/api/paymob.ts new file mode 100644 index 00000000..2cab06cd --- /dev/null +++ b/src/pages/api/paymob.ts @@ -0,0 +1,101 @@ +// 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; +}; diff --git a/yarn.lock b/yarn.lock index cfab02ba..043500ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4870,6 +4870,13 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +"paymob-react@git+https://github.com/tiago-ecrop/paymob-react-oman.git": + version "1.0.0" + resolved "git+https://github.com/tiago-ecrop/paymob-react-oman.git#9e7d1e86f01d29dd10192bbd371517849a264e5d" + dependencies: + react "^18.2.0" + react-dom "^18.2.0" + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" @@ -5201,6 +5208,14 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-dom@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + react-fast-compare@^3.0.1: version "3.2.1" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz" @@ -5330,6 +5345,13 @@ react@18.2.0: dependencies: loose-envify "^1.1.0" +react@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" @@ -5535,6 +5557,13 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + seedrandom@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" From 7af96eccccf30f9c6a27a98ecca81cda877cc412 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 15 May 2024 00:25:44 +0100 Subject: [PATCH 03/12] Created a webhook to allow the transaction to be completed --- src/components/PaymobPayment.tsx | 94 +++++++++++++++++++----- src/interfaces/paymob.ts | 118 ++++++++++++++++++++++++++++++ src/pages/(status)/PaymentDue.tsx | 111 +++++++++++++--------------- src/pages/api/paymob.ts | 101 ------------------------- src/pages/api/paymob/index.ts | 52 +++++++++++++ src/pages/api/paymob/webhook.ts | 82 +++++++++++++++++++++ 6 files changed, 378 insertions(+), 180 deletions(-) create mode 100644 src/interfaces/paymob.ts delete mode 100644 src/pages/api/paymob.ts create mode 100644 src/pages/api/paymob/index.ts create mode 100644 src/pages/api/paymob/webhook.ts diff --git a/src/components/PaymobPayment.tsx b/src/components/PaymobPayment.tsx index fe27543f..eaec5a78 100644 --- a/src/components/PaymobPayment.tsx +++ b/src/components/PaymobPayment.tsx @@ -1,38 +1,73 @@ +import {PaymentIntention} from "@/interfaces/paymob"; import {DurationUnit} from "@/interfaces/paypal"; import {User} from "@/interfaces/user"; import axios from "axios"; import {useState} from "react"; import Button from "./Low/Button"; +import Input from "./Low/Input"; +import Modal from "./Modal"; interface Props { user: User; currency: string; price: number; - title: string; - description: string; - paymentID: string; + packageID: string; + setIsPaymentLoading: (v: boolean) => void; duration: number; duration_unit: DurationUnit; - setIsLoading: (isLoading: boolean) => void; onSuccess: (duration: number, duration_unit: DurationUnit) => void; } -export default function PaymobPayment({ - user, - price, - currency, - title, - description, - paymentID, - duration, - duration_unit, - setIsLoading, - onSuccess, -}: Props) { - const [iframeURL, setIFrameURL] = useState(); +export default function PaymobPayment({user, price, packageID, setIsPaymentLoading, currency, duration, duration_unit, onSuccess}: Props) { + const [isLoading, setIsLoading] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + + const [firstName, setFirstName] = useState(user.name.split(" ")[0]); + const [lastName, setLastName] = useState([...user.name.split(" ")].pop()); + const [street, setStreet] = useState(""); + const [apartment, setApartment] = useState(""); + const [building, setBuilding] = useState(""); + const [state, setState] = useState(""); + const [floor, setFloor] = useState(""); const handleCardPayment = async () => { 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) { console.error("Error starting card payment process:", error); } @@ -40,8 +75,29 @@ export default function PaymobPayment({ return ( <> - - {iframeURL} + setIsModalOpen(false)}> +
+
+ + +
+
+ + + +
+
+ + +
+ +
+
+ ); } diff --git a/src/interfaces/paymob.ts b/src/interfaces/paymob.ts new file mode 100644 index 00000000..c5dce4b7 --- /dev/null +++ b/src/interfaces/paymob.ts @@ -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; +} diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 3cb5b680..a5920a92 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -107,44 +107,37 @@ 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:
- - {packages.map((p) => ( -
-
- EnCoach's Logo - - EnCoach - {p.duration}{" "} - {capitalize( - p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, - )} + {packages.map((p) => ( +
+
+ EnCoach's Logo + + EnCoach - {p.duration}{" "} + {capitalize( + p.duration === 1 ? p.duration_unit.slice(0, p.duration_unit.length - 1) : p.duration_unit, + )} + +
+
+ {!appliedDiscount && ( + + {p.price} + {getSymbolFromCurrency(p.currency)} -
-
- {!appliedDiscount && ( - + )} + {appliedDiscount && ( +
+ {p.price} {getSymbolFromCurrency(p.currency)} - )} - {appliedDiscount && ( -
- - {p.price} - {getSymbolFromCurrency(p.currency)} - - - {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} - {getSymbolFromCurrency(p.currency)} - -
- )} - {/* + {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} + {getSymbolFromCurrency(p.currency)} + +
+ )} + {/* */} - { - setTimeout(reload, 500); - }} - currency={p.currency} - duration={p.duration} - duration_unit={p.duration_unit} - price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} - /> -
-
- This includes: -
    -
  • - Train your abilities for the IELTS exam
  • -
  • - Gain insights into your weaknesses and strengths
  • -
  • - Allow yourself to correctly prepare for the exam
  • -
-
+ { + setTimeout(reload, 500); + }} + currency={p.currency} + duration={p.duration} + duration_unit={p.duration_unit} + price={+(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} + />
- ))} - +
+ This includes: +
    +
  • - Train your abilities for the IELTS exam
  • +
  • - Gain insights into your weaknesses and strengths
  • +
  • - Allow yourself to correctly prepare for the exam
  • +
+
+
+ ))} + z
)} diff --git a/src/pages/api/paymob.ts b/src/pages/api/paymob.ts deleted file mode 100644 index 2cab06cd..00000000 --- a/src/pages/api/paymob.ts +++ /dev/null @@ -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; -}; diff --git a/src/pages/api/paymob/index.ts b/src/pages/api/paymob/index.ts new file mode 100644 index 00000000..58bd7c85 --- /dev/null +++ b/src/pages/api/paymob/index.ts @@ -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( + "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}`, + }); +} diff --git a/src/pages/api/paymob/webhook.ts b/src/pages/api/paymob/webhook.ts new file mode 100644 index 00000000..257afdbe --- /dev/null +++ b/src/pages/api/paymob/webhook.ts @@ -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; +}; From 2920fa7f3af366c79cb7432662e4cf6f78fb2116 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 15 May 2024 22:59:51 +0100 Subject: [PATCH 04/12] Updated the payment to work with Paymob --- src/components/Navbar.tsx | 2 +- src/components/PaymobPayment.tsx | 12 +- src/interfaces/paymob.ts | 2 +- src/pages/(status)/PaymentDue.tsx | 44 +- src/pages/api/paymob/webhook.ts | 27 +- src/pages/payment-record.tsx | 2489 +++++++++++++---------------- 6 files changed, 1164 insertions(+), 1412 deletions(-) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 4a2caf65..02f82022 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -86,7 +86,7 @@ export default function Navbar({user, path, navDisabled = false, focusMode = fal {showExpirationDate() && ( void; duration: number; duration_unit: DurationUnit; onSuccess: (duration: number, duration_unit: DurationUnit) => void; } -export default function PaymobPayment({user, price, packageID, setIsPaymentLoading, currency, duration, duration_unit, onSuccess}: Props) { +export default function PaymobPayment({user, price, setIsPaymentLoading, currency, duration, duration_unit, onSuccess}: Props) { const [isLoading, setIsLoading] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); @@ -30,6 +30,8 @@ export default function PaymobPayment({user, price, packageID, setIsPaymentLoadi const [state, setState] = useState(""); const [floor, setFloor] = useState(""); + const router = useRouter(); + const handleCardPayment = async () => { try { setIsPaymentLoading(true); @@ -61,13 +63,15 @@ export default function PaymobPayment({user, price, packageID, setIsPaymentLoadi }, extras: { userID: user.id, - packageID: packageID, + duration, + duration_unit, }, }; const response = await axios.post<{iframeURL: string}>(`/api/paymob`, paymentIntention); - window.open(response.data.iframeURL, "_blank", "noopener,noreferrer"); + router.push(response.data.iframeURL); + setIsModalOpen(false); } catch (error) { console.error("Error starting card payment process:", error); } diff --git a/src/interfaces/paymob.ts b/src/interfaces/paymob.ts index c5dce4b7..f75445a4 100644 --- a/src/interfaces/paymob.ts +++ b/src/interfaces/paymob.ts @@ -28,7 +28,7 @@ interface Customer { extras: IntentionExtras; } -type IntentionExtras = {[key: string]: string}; +type IntentionExtras = {[key: string]: string | number}; export interface IntentionResult { payment_keys: PaymentKeysItem[]; diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index a5920a92..5a5f00d8 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -1,6 +1,5 @@ /* eslint-disable @next/next/no-img-element */ import Layout from "@/components/High/Layout"; -import PayPalPayment from "@/components/PayPalPayment"; import useGroups from "@/hooks/useGroups"; import usePackages from "@/hooks/usePackages"; import useUsers from "@/hooks/useUsers"; @@ -13,8 +12,6 @@ import useInvites from "@/hooks/useInvites"; import {BsArrowRepeat} from "react-icons/bs"; import InviteCard from "@/components/Medium/InviteCard"; import {useRouter} from "next/router"; -import {PayPalScriptProvider} from "@paypal/react-paypal-js"; -import {usePaypalTracking} from "@/hooks/usePaypalTracking"; import {ToastContainer} from "react-toastify"; import useDiscounts from "@/hooks/useDiscounts"; import PaymobPayment from "@/components/PaymobPayment"; @@ -37,7 +34,6 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}: const {users} = useUsers(); const {groups} = useGroups(); const {invites, isLoading: isInvitesLoading, reload: reloadInvites} = useInvites({to: user?.id}); - const trackingId = usePaypalTracking(); useEffect(() => { const userDiscounts = discounts.filter((x) => user.email.endsWith(`@${x.domain}`)); @@ -65,9 +61,15 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}: {isLoading && (
-
- - Completing your payment... +
+ + Completing your payment... + If you canceled your payment or it failed, please click the button below to restart +
)} @@ -137,29 +139,10 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
)} - {/* { - setTimeout(reload, 500); - }} - trackingId={trackingId} - currency={p.currency} - duration={p.duration} - duration_unit={p.duration_unit} - price={ - +( - p.price - - p.price * (appliedDiscount / 100) - ).toFixed(2) - } - /> */} { setTimeout(reload, 500); }} @@ -179,7 +162,6 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
))} - z )} @@ -198,10 +180,10 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}: {user.corporateInformation.payment.value} {getSymbolFromCurrency(user.corporateInformation.payment.currency)} -
diff --git a/src/pages/api/paymob/webhook.ts b/src/pages/api/paymob/webhook.ts index 257afdbe..749dbd68 100644 --- a/src/pages/api/paymob/webhook.ts +++ b/src/pages/api/paymob/webhook.ts @@ -5,7 +5,7 @@ import {getFirestore, collection, getDocs, setDoc, doc, getDoc, query, where} fr import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {Group, User} from "@/interfaces/user"; -import {Package, Payment} from "@/interfaces/paypal"; +import {DurationUnit, Package, Payment} from "@/interfaces/paypal"; import {v4} from "uuid"; import ShortUniqueId from "short-unique-id"; import axios from "axios"; @@ -25,24 +25,37 @@ async function post(req: NextApiRequest, res: NextApiResponse) { 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 {userID, duration, duration_unit} = transactionResult.intention.extras.creation_extras as { + userID: string; + duration: number; + duration_unit: DurationUnit; + }; - const userSnapshot = await getDoc(doc(db, "users", userID)); - const packageSnapshot = await getDoc(doc(db, "packages", packageID)); + const userSnapshot = await getDoc(doc(db, "users", userID as string)); - if (!userSnapshot.exists() || !packageSnapshot.exists()) return res.status(404).json({ok: false}); + if (!userSnapshot.exists() || !duration || !duration_unit) 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(); + const updatedSubscriptionExpirationDate = moment(initialDate).add(duration, duration_unit).toISOString(); await setDoc(userSnapshot.ref, {subscriptionExpirationDate: updatedSubscriptionExpirationDate}, {merge: true}); + await setDoc(doc(db, "paypalpayments", v4()), { + createdAt: new Date().toISOString(), + currency: transactionResult.transaction.currency, + orderId: transactionResult.transaction.id, + status: "COMPLETED", + subscriptionDuration: duration, + subscriptionDurationUnit: duration_unit, + subscriptionExpirationDate: updatedSubscriptionExpirationDate, + userId: userID, + value: transactionResult.transaction.amount_cents / 1000, + }); if (user.type === "corporate") { const groupsSnapshot = await getDocs(query(collection(db, "groups"), where("admin", "==", user.id))); diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 4f9350fe..50837f1b 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -1,28 +1,20 @@ /* eslint-disable @next/next/no-img-element */ import Head from "next/head"; -import { withIronSessionSsr } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; +import {withIronSessionSsr} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; import useUser from "@/hooks/useUser"; -import { toast, ToastContainer } from "react-toastify"; +import {toast, ToastContainer} from "react-toastify"; import Layout from "@/components/High/Layout"; -import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; import usePayments from "@/hooks/usePayments"; import usePaypalPayments from "@/hooks/usePaypalPayments"; -import { Payment, PaypalPayment } from "@/interfaces/paypal"; -import { - CellContext, - createColumnHelper, - flexRender, - getCoreRowModel, - HeaderGroup, - Table, - useReactTable, -} from "@tanstack/react-table"; -import { CURRENCIES } from "@/resources/paypal"; -import { BsTrash } from "react-icons/bs"; +import {Payment, PaypalPayment} from "@/interfaces/paypal"; +import {CellContext, createColumnHelper, flexRender, getCoreRowModel, HeaderGroup, Table, useReactTable} from "@tanstack/react-table"; +import {CURRENCIES} from "@/resources/paypal"; +import {BsTrash} from "react-icons/bs"; import axios from "axios"; -import { useEffect, useState, useMemo } from "react"; -import { AgentUser, CorporateUser, User } from "@/interfaces/user"; +import {useEffect, useState, useMemo} from "react"; +import {AgentUser, CorporateUser, User} from "@/interfaces/user"; import UserCard from "@/components/UserCard"; import Modal from "@/components/Modal"; import clsx from "clsx"; @@ -34,1429 +26,1192 @@ import Input from "@/components/Low/Input"; import ReactDatePicker from "react-datepicker"; import moment from "moment"; import PaymentAssetManager from "@/components/PaymentAssetManager"; -import { toFixedNumber } from "@/utils/number"; -import { CSVLink } from "react-csv"; -import { Tab } from "@headlessui/react"; -import { useListSearch } from "@/hooks/useListSearch"; -export const getServerSideProps = withIronSessionSsr(({ req, res }) => { - const user = req.session.user; +import {toFixedNumber} from "@/utils/number"; +import {CSVLink} from "react-csv"; +import {Tab} from "@headlessui/react"; +import {useListSearch} from "@/hooks/useListSearch"; +export const getServerSideProps = withIronSessionSsr(({req, res}) => { + const user = req.session.user; - if (!user || !user.isVerified) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } + if (!user || !user.isVerified) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } - if ( - shouldRedirectHome(user) || - !["admin", "developer", "agent", "corporate"].includes(user.type) - ) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } + if (shouldRedirectHome(user) || !["admin", "developer", "agent", "corporate"].includes(user.type)) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } - return { - props: { user: req.session.user }, - }; + return { + props: {user: req.session.user}, + }; }, sessionOptions); const columnHelper = createColumnHelper(); const paypalColumnHelper = createColumnHelper(); -const PaymentCreator = ({ - onClose, - reload, - showComission = false, -}: { - onClose: () => void; - reload: () => void; - showComission: boolean; -}) => { - const [corporate, setCorporate] = useState(); - const [date, setDate] = useState(new Date()); +const PaymentCreator = ({onClose, reload, showComission = false}: {onClose: () => void; reload: () => void; showComission: boolean}) => { + const [corporate, setCorporate] = useState(); + const [date, setDate] = useState(new Date()); - const { users } = useUsers(); + const {users} = useUsers(); - const price = corporate?.corporateInformation?.payment?.value || 0; - const commission = corporate?.corporateInformation?.payment?.commission || 0; - const currency = corporate?.corporateInformation?.payment?.currency || "EUR"; + const price = corporate?.corporateInformation?.payment?.value || 0; + const commission = corporate?.corporateInformation?.payment?.commission || 0; + const currency = corporate?.corporateInformation?.payment?.currency || "EUR"; - const referralAgent = useMemo(() => { - if (corporate?.corporateInformation?.referralAgent) { - return users.find( - (u) => u.id === corporate.corporateInformation.referralAgent, - ); - } + const referralAgent = useMemo(() => { + if (corporate?.corporateInformation?.referralAgent) { + return users.find((u) => u.id === corporate.corporateInformation.referralAgent); + } - return undefined; - }, [corporate?.corporateInformation?.referralAgent, users]); + return undefined; + }, [corporate?.corporateInformation?.referralAgent, users]); - const submit = () => { - axios - .post(`/api/payments`, { - corporate: corporate?.id, - agent: referralAgent?.id, - agentCommission: commission, - agentValue: toFixedNumber((commission! / 100) * price!, 2), - currency, - value: price, - isPaid: false, - date: date.toISOString(), - }) - .then(() => { - toast.success("New payment has been created successfully!"); - reload(); - onClose(); - }) - .catch(() => { - toast.error("Something went wrong, please try again later!"); - }); - }; + const submit = () => { + axios + .post(`/api/payments`, { + corporate: corporate?.id, + agent: referralAgent?.id, + agentCommission: commission, + agentValue: toFixedNumber((commission! / 100) * price!, 2), + currency, + value: price, + isPaid: false, + date: date.toISOString(), + }) + .then(() => { + toast.success("New payment has been created successfully!"); + reload(); + onClose(); + }) + .catch(() => { + toast.error("Something went wrong, please try again later!"); + }); + }; - return ( -
-

New Payment

-
-
- - u.type === "corporate") as CorporateUser[]).map((user) => ({ + value: user.id, + meta: user, + label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`, + }))} + defaultValue={{value: "undefined", label: "Select an account"}} + onChange={(value) => setCorporate((value as any)?.meta ?? undefined)} + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({...base, zIndex: 9999}), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
-
- -
- {}} - type="number" - value={price} - defaultValue={0} - className="col-span-3" - disabled - /> - {}} - type="number" - defaultValue={0} - value={commission} - disabled - /> -
-
- - c.currency === currency)?.label}`} - onChange={() => null} - type="text" - defaultValue={0} - disabled - /> -
-
- )} +
+ +
+ {}} type="number" value={price} defaultValue={0} className="col-span-3" disabled /> + {}} type="number" defaultValue={0} value={commission} disabled /> +
+
+ + c.currency === currency)?.label}`} + onChange={() => null} + type="text" + defaultValue={0} + disabled + /> +
+
+ )} -
-
- - setDate(date ?? new Date())} - /> -
+
+
+ + setDate(date ?? new Date())} + /> +
-
- - null} - type="text" - defaultValue={"No country manager"} - disabled - /> -
-
-
- - -
-
-
- ); +
+ + null} + type="text" + defaultValue={"No country manager"} + disabled + /> +
+
+
+ + +
+
+ + ); }; const IS_PAID_OPTIONS = [ - { - value: null, - label: "All", - }, - { - value: false, - label: "Unpaid", - }, - { - value: true, - label: "Paid", - }, + { + value: null, + label: "All", + }, + { + value: false, + label: "Unpaid", + }, + { + value: true, + label: "Paid", + }, ]; const IS_FILE_SUBMITTED_OPTIONS = [ - { - value: null, - label: "All", - }, - { - value: false, - label: "Submitted", - }, - { - value: true, - label: "Not Submitted", - }, + { + value: null, + label: "All", + }, + { + value: false, + label: "Submitted", + }, + { + value: true, + label: "Not Submitted", + }, ]; -const CSV_PAYMENTS_WHITELISTED_KEYS = [ - "corporateId", - "corporate", - "date", - "amount", - "agent", - "agentCommission", - "agentValue", - "isPaid", -]; +const CSV_PAYMENTS_WHITELISTED_KEYS = ["corporateId", "corporate", "date", "amount", "agent", "agentCommission", "agentValue", "isPaid"]; -const CSV_PAYPAL_WHITELISTED_KEYS = [ - "orderId", - "status", - "name", - "email", - "value", - "createdAt", - "subscriptionExpirationDate", -]; +const CSV_PAYPAL_WHITELISTED_KEYS = ["orderId", "status", "name", "email", "value", "createdAt", "subscriptionExpirationDate"]; interface SimpleCSVColumn { - key: string; - label: string; - index: number; + key: string; + label: string; + index: number; } interface PaypalPaymentWithUserData extends PaypalPayment { - name: string; - email: string; + name: string; + email: string; } const paypalFilterRows = [["email"], ["name"]]; export default function PaymentRecord() { - const [selectedCorporateUser, setSelectedCorporateUser] = useState(); - const [selectedAgentUser, setSelectedAgentUser] = useState(); - const [isCreatingPayment, setIsCreatingPayment] = useState(false); - const [filters, setFilters] = useState< - { filter: (p: Payment) => boolean; id: string }[] - >([]); - const [displayPayments, setDisplayPayments] = useState([]); + const [selectedCorporateUser, setSelectedCorporateUser] = useState(); + const [selectedAgentUser, setSelectedAgentUser] = useState(); + const [isCreatingPayment, setIsCreatingPayment] = useState(false); + const [filters, setFilters] = useState<{filter: (p: Payment) => boolean; id: string}[]>([]); + const [displayPayments, setDisplayPayments] = useState([]); - const [corporate, setCorporate] = useState(); - const [agent, setAgent] = useState(); + const [corporate, setCorporate] = useState(); + const [agent, setAgent] = useState(); - const { user } = useUser({ redirectTo: "/login" }); - const { users, reload: reloadUsers } = useUsers(); - const { payments: originalPayments, reload: reloadPayment } = usePayments(); - const { payments: paypalPayments, reload: reloadPaypalPayment } = - usePaypalPayments(); - const [startDate, setStartDate] = useState( - moment("01/01/2023").toDate(), - ); - const [endDate, setEndDate] = useState( - moment().endOf("day").toDate(), - ); - const [paid, setPaid] = useState(IS_PAID_OPTIONS[0].value); - const [commissionTransfer, setCommissionTransfer] = useState( - IS_FILE_SUBMITTED_OPTIONS[0].value, - ); - const [corporateTransfer, setCorporateTransfer] = useState( - IS_FILE_SUBMITTED_OPTIONS[0].value, - ); - const reload = () => { - reloadUsers(); - reloadPayment(); - }; + const {user} = useUser({redirectTo: "/login"}); + const {users, reload: reloadUsers} = useUsers(); + const {payments: originalPayments, reload: reloadPayment} = usePayments(); + const {payments: paypalPayments, reload: reloadPaypalPayment} = usePaypalPayments(); + const [startDate, setStartDate] = useState(moment("01/01/2023").toDate()); + const [endDate, setEndDate] = useState(moment().endOf("day").toDate()); + const [paid, setPaid] = useState(IS_PAID_OPTIONS[0].value); + const [commissionTransfer, setCommissionTransfer] = useState(IS_FILE_SUBMITTED_OPTIONS[0].value); + const [corporateTransfer, setCorporateTransfer] = useState(IS_FILE_SUBMITTED_OPTIONS[0].value); + const reload = () => { + reloadUsers(); + reloadPayment(); + }; - const payments = useMemo(() => { - return originalPayments.filter((p: Payment) => { - const date = moment(p.date); - return date.isAfter(startDate) && date.isBefore(endDate); - }); - }, [originalPayments, startDate, endDate]); + const payments = useMemo(() => { + return originalPayments.filter((p: Payment) => { + const date = moment(p.date); + return date.isAfter(startDate) && date.isBefore(endDate); + }); + }, [originalPayments, startDate, endDate]); - const [selectedIndex, setSelectedIndex] = useState(0); + const [selectedIndex, setSelectedIndex] = useState(0); - useEffect(() => { - setDisplayPayments( - filters - .map((f) => f.filter) - .reduce((d, f) => d.filter(f), payments) - .sort((a, b) => moment(b.date).diff(moment(a.date))), - ); - }, [payments, filters]); + useEffect(() => { + setDisplayPayments( + filters + .map((f) => f.filter) + .reduce((d, f) => d.filter(f), payments) + .sort((a, b) => moment(b.date).diff(moment(a.date))), + ); + }, [payments, filters]); - useEffect(() => { - if (user && user.type === "agent") { - setAgent(user); - } - }, [user]); + useEffect(() => { + if (user && user.type === "agent") { + setAgent(user); + } + }, [user]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "agent-filter"), - ...(!agent - ? [] - : [ - { - id: "agent-filter", - filter: (p: Payment) => p.agent === agent.id, - }, - ]), - ]); - }, [agent]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "agent-filter"), + ...(!agent + ? [] + : [ + { + id: "agent-filter", + filter: (p: Payment) => p.agent === agent.id, + }, + ]), + ]); + }, [agent]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "corporate-filter"), - ...(!corporate - ? [] - : [ - { - id: "corporate-filter", - filter: (p: Payment) => p.corporate === corporate.id, - }, - ]), - ]); - }, [corporate]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "corporate-filter"), + ...(!corporate + ? [] + : [ + { + id: "corporate-filter", + filter: (p: Payment) => p.corporate === corporate.id, + }, + ]), + ]); + }, [corporate]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "paid"), - ...(typeof paid !== "boolean" - ? [] - : [{ id: "paid", filter: (p: Payment) => p.isPaid === paid }]), - ]); - }, [paid]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "paid"), + ...(typeof paid !== "boolean" ? [] : [{id: "paid", filter: (p: Payment) => p.isPaid === paid}]), + ]); + }, [paid]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "commissionTransfer"), - ...(typeof commissionTransfer !== "boolean" - ? [] - : [ - { - id: "commissionTransfer", - filter: (p: Payment) => - !p.commissionTransfer === commissionTransfer, - }, - ]), - ]); - }, [commissionTransfer]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "commissionTransfer"), + ...(typeof commissionTransfer !== "boolean" + ? [] + : [ + { + id: "commissionTransfer", + filter: (p: Payment) => !p.commissionTransfer === commissionTransfer, + }, + ]), + ]); + }, [commissionTransfer]); - useEffect(() => { - setFilters((prev) => [ - ...prev.filter((x) => x.id !== "corporateTransfer"), - ...(typeof corporateTransfer !== "boolean" - ? [] - : [ - { - id: "corporateTransfer", - filter: (p: Payment) => - !p.corporateTransfer === corporateTransfer, - }, - ]), - ]); - }, [corporateTransfer]); + useEffect(() => { + setFilters((prev) => [ + ...prev.filter((x) => x.id !== "corporateTransfer"), + ...(typeof corporateTransfer !== "boolean" + ? [] + : [ + { + id: "corporateTransfer", + filter: (p: Payment) => !p.corporateTransfer === corporateTransfer, + }, + ]), + ]); + }, [corporateTransfer]); - useEffect(() => { - if (user && user.type === "corporate") return setCorporate(user); - if (user && user.type === "agent") return setAgent(user); - }, [user]); + useEffect(() => { + if (user && user.type === "corporate") return setCorporate(user); + if (user && user.type === "agent") return setAgent(user); + }, [user]); - const updatePayment = (payment: Payment, key: string, value: any) => { - axios - .patch(`api/payments/${payment.id}`, { ...payment, [key]: value }) - .then(() => toast.success("Updated the payment")) - .finally(reload); - }; + const updatePayment = (payment: Payment, key: string, value: any) => { + axios + .patch(`api/payments/${payment.id}`, {...payment, [key]: value}) + .then(() => toast.success("Updated the payment")) + .finally(reload); + }; - const deletePayment = (id: string) => { - if (!confirm(`Are you sure you want to delete this payment?`)) return; + const deletePayment = (id: string) => { + if (!confirm(`Are you sure you want to delete this payment?`)) return; - axios - .delete(`/api/payments/${id}`) - .then(() => toast.success(`Deleted the "${id}" payment`)) - .catch((reason) => { - if (reason.response.status === 404) { - toast.error("Exam not found!"); - return; - } + axios + .delete(`/api/payments/${id}`) + .then(() => toast.success(`Deleted the "${id}" payment`)) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Exam not found!"); + return; + } - if (reason.response.status === 403) { - toast.error( - "You do not have permission to delete an approved payment record!", - ); - return; - } + if (reason.response.status === 403) { + toast.error("You do not have permission to delete an approved payment record!"); + return; + } - toast.error("Something went wrong, please try again later."); - }) - .finally(reload); - }; + toast.error("Something went wrong, please try again later."); + }) + .finally(reload); + }; - const getFileAssetsColumns = () => { - if (user) { - const containerClassName = - "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer"; - switch (user.type) { - case "corporate": - return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - id: "corporateTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - case "agent": - return [ - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - id: "commissionTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - case "admin": - return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - id: "corporateTransfer", - cell: (info) => ( -
- -
- ), - }), - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - id: "commissionTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - case "developer": - return [ - columnHelper.accessor("corporateTransfer", { - header: "Corporate transfer", - id: "corporateTransfer", - cell: (info) => ( -
- -
- ), - }), - columnHelper.accessor("commissionTransfer", { - header: "Commission transfer", - id: "commissionTransfer", - cell: (info) => ( -
- -
- ), - }), - ]; - default: - return []; - } - } + const getFileAssetsColumns = () => { + if (user) { + const containerClassName = "flex gap-2 text-mti-purple-light hover:text-mti-purple-dark ease-in-out duration-300 cursor-pointer"; + switch (user.type) { + case "corporate": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + id: "corporateTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "agent": + return [ + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + id: "commissionTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "admin": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + id: "corporateTransfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + id: "commissionTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + case "developer": + return [ + columnHelper.accessor("corporateTransfer", { + header: "Corporate transfer", + id: "corporateTransfer", + cell: (info) => ( +
+ +
+ ), + }), + columnHelper.accessor("commissionTransfer", { + header: "Commission transfer", + id: "commissionTransfer", + cell: (info) => ( +
+ +
+ ), + }), + ]; + default: + return []; + } + } - return []; - }; + return []; + }; - const columHelperValue = (key: string, info: any) => { - switch (key) { - case "agentCommission": { - const value = info.getValue(); - return { value: `${value}%` }; - } - case "agent": { - const user = users.find( - (x) => x.id === info.row.original.agent, - ) as AgentUser; - return { - value: user?.name, - user, - }; - } - case "agentValue": - case "amount": { - const value = info.getValue(); - const numberValue = toFixedNumber(value, 2); - return { value: numberValue }; - } - case "date": { - const value = info.getValue(); - return { value: moment(value).format("DD/MM/YYYY") }; - } - case "corporate": { - const specificValue = info.row.original.corporate; - const user = users.find((x) => x.id === specificValue) as CorporateUser; - return { - user, - value: - user?.corporateInformation.companyInformation.name || user?.name, - }; - } - case "currency": { - return { - value: info.row.original.currency, - }; - } - case "isPaid": - case "corporateId": - default: { - const value = info.getValue(); - return { value }; - } - } - }; + const columHelperValue = (key: string, info: any) => { + switch (key) { + case "agentCommission": { + const value = info.getValue(); + return {value: `${value}%`}; + } + case "agent": { + const user = users.find((x) => x.id === info.row.original.agent) as AgentUser; + return { + value: user?.name, + user, + }; + } + case "agentValue": + case "amount": { + const value = info.getValue(); + const numberValue = toFixedNumber(value, 2); + return {value: numberValue}; + } + case "date": { + const value = info.getValue(); + return {value: moment(value).format("DD/MM/YYYY")}; + } + case "corporate": { + const specificValue = info.row.original.corporate; + const user = users.find((x) => x.id === specificValue) as CorporateUser; + return { + user, + value: user?.corporateInformation.companyInformation.name || user?.name, + }; + } + case "currency": { + return { + value: info.row.original.currency, + }; + } + case "isPaid": + case "corporateId": + default: { + const value = info.getValue(); + return {value}; + } + } + }; - const hiddenToCorporateColumns = () => { - if (user && user.type !== "corporate") - return [ - columnHelper.accessor("agent", { - header: "Country Manager", - id: "agent", - cell: (info) => { - const { user, value } = columHelperValue(info.column.id, info); - return ( -
setSelectedAgentUser(user)} - > - {value} -
- ); - }, - }), - columnHelper.accessor("agentCommission", { - header: "Commission", - id: "agentCommission", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return <>{value}; - }, - }), - columnHelper.accessor("agentValue", { - header: "Commission Value", - id: "agentValue", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - const currency = CURRENCIES.find( - (x) => x.currency === info.row.original.currency, - )?.label; - const finalValue = `${value} ${currency}`; - return {finalValue}; - }, - }), - ]; - return []; - }; + const hiddenToCorporateColumns = () => { + if (user && user.type !== "corporate") + return [ + columnHelper.accessor("agent", { + header: "Country Manager", + id: "agent", + cell: (info) => { + const {user, value} = columHelperValue(info.column.id, info); + return ( +
setSelectedAgentUser(user)}> + {value} +
+ ); + }, + }), + columnHelper.accessor("agentCommission", { + header: "Commission", + id: "agentCommission", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return <>{value}; + }, + }), + columnHelper.accessor("agentValue", { + header: "Commission Value", + id: "agentValue", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + const currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label; + const finalValue = `${value} ${currency}`; + return {finalValue}; + }, + }), + ]; + return []; + }; - const defaultColumns = [ - columnHelper.accessor("corporate", { - header: "Corporate ID", - id: "corporateId", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return value; - }, - }), - columnHelper.accessor("corporate", { - header: "Corporate", - id: "corporate", - cell: (info) => { - const { user, value } = columHelperValue(info.column.id, info); - return ( -
setSelectedCorporateUser(user)} - > - {value} -
- ); - }, - }), - columnHelper.accessor("date", { - header: "Date", - id: "date", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return {value}; - }, - }), - columnHelper.accessor("value", { - header: "Amount", - id: "amount", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - const currency = CURRENCIES.find( - (x) => x.currency === info.row.original.currency, - )?.label; - const finalValue = `${value} ${currency}`; - return {finalValue}; - }, - }), - ...hiddenToCorporateColumns(), - columnHelper.accessor("isPaid", { - header: "Paid", - id: "isPaid", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); + const defaultColumns = [ + columnHelper.accessor("corporate", { + header: "Corporate ID", + id: "corporateId", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return value; + }, + }), + columnHelper.accessor("corporate", { + header: "Corporate", + id: "corporate", + cell: (info) => { + const {user, value} = columHelperValue(info.column.id, info); + return ( +
setSelectedCorporateUser(user)}> + {value} +
+ ); + }, + }), + columnHelper.accessor("date", { + header: "Date", + id: "date", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return {value}; + }, + }), + columnHelper.accessor("value", { + header: "Amount", + id: "amount", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + const currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label; + const finalValue = `${value} ${currency}`; + return {finalValue}; + }, + }), + ...hiddenToCorporateColumns(), + columnHelper.accessor("isPaid", { + header: "Paid", + id: "isPaid", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); - return ( - { - if (user?.type === agent || user?.type === "corporate" || value) - return null; - if ( - !info.row.original.commissionTransfer || - !info.row.original.corporateTransfer - ) - return alert( - "All files need to be uploaded to consider it paid!", - ); - if ( - !confirm(`Are you sure you want to consider this payment paid?`) - ) - return null; + return ( + { + if (user?.type === agent || user?.type === "corporate" || value) return null; + if (!info.row.original.commissionTransfer || !info.row.original.corporateTransfer) + return alert("All files need to be uploaded to consider it paid!"); + if (!confirm(`Are you sure you want to consider this payment paid?`)) return null; - return updatePayment(info.row.original, "isPaid", e); - }} - > - - - ); - }, - }), - ...getFileAssetsColumns(), - { - header: "", - id: "actions", - cell: ({ row }: { row: { original: Payment } }) => { - return ( -
- {user?.type !== "agent" && ( -
deletePayment(row.original.id)} - > - -
- )} -
- ); - }, - }, - ]; + return updatePayment(info.row.original, "isPaid", e); + }}> + +
+ ); + }, + }), + ...getFileAssetsColumns(), + { + header: "", + id: "actions", + cell: ({row}: {row: {original: Payment}}) => { + return ( +
+ {user?.type !== "agent" && ( +
deletePayment(row.original.id)}> + +
+ )} +
+ ); + }, + }, + ]; - const table = useReactTable({ - data: displayPayments, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - }); + const table = useReactTable({ + data: displayPayments, + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + }); - const updatedPaypalPayments = useMemo( - () => - paypalPayments.map((p) => { - const user = users.find((x) => x.id === p.userId) as User; - return { ...p, name: user?.name, email: user?.email }; - }), - [paypalPayments, users], - ); + const updatedPaypalPayments = useMemo( + () => + paypalPayments.map((p) => { + const user = users.find((x) => x.id === p.userId) as User; + return {...p, name: user?.name, email: user?.email}; + }), + [paypalPayments, users], + ); - const paypalColumns = [ - paypalColumnHelper.accessor("orderId", { - header: "Order ID", - id: "orderId", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("status", { - header: "Status", - id: "status", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("name", { - header: "User Name", - id: "name", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("email", { - header: "Email", - id: "email", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return {value}; - }, - }), - paypalColumnHelper.accessor("value", { - header: "Amount", - id: "value", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - const currency = CURRENCIES.find( - (x) => x.currency === info.row.original.currency, - )?.label; - const finalValue = `${value} ${currency}`; - return {finalValue}; - }, - }), - paypalColumnHelper.accessor("createdAt", { - header: "Date", - id: "createdAt", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return {moment(value).format("DD/MM/YYYY")}; - }, - }), - paypalColumnHelper.accessor("subscriptionExpirationDate", { - header: "Expiration Date", - id: "subscriptionExpirationDate", - cell: (info) => { - const { value } = columHelperValue(info.column.id, info); - return {moment(value).format("DD/MM/YYYY")}; - }, - }), - ]; + const paypalColumns = [ + paypalColumnHelper.accessor("orderId", { + header: "Order ID", + id: "orderId", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("status", { + header: "Status", + id: "status", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("name", { + header: "User Name", + id: "name", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("email", { + header: "Email", + id: "email", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return {value}; + }, + }), + paypalColumnHelper.accessor("value", { + header: "Amount", + id: "value", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + const finalValue = `${value} ${info.row.original.currency}`; + return {finalValue}; + }, + }), + paypalColumnHelper.accessor("createdAt", { + header: "Date", + id: "createdAt", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return {moment(value).format("DD/MM/YYYY")}; + }, + }), + paypalColumnHelper.accessor("subscriptionExpirationDate", { + header: "Expiration Date", + id: "subscriptionExpirationDate", + cell: (info) => { + const {value} = columHelperValue(info.column.id, info); + return {moment(value).format("DD/MM/YYYY")}; + }, + }), + ]; - const { rows: filteredRows, renderSearch } = useListSearch( - paypalFilterRows, - updatedPaypalPayments, - ); + const {rows: filteredRows, renderSearch} = useListSearch(paypalFilterRows, updatedPaypalPayments); - const paypalTable = useReactTable({ - data: filteredRows, - columns: paypalColumns, - getCoreRowModel: getCoreRowModel(), - }); - const getUserModal = () => { - if (user) { - if (selectedCorporateUser) { - return ( - setSelectedCorporateUser(undefined)} - > - <> - {selectedCorporateUser && ( -
- { - setSelectedCorporateUser(undefined); - if (shouldReload) reload(); - }} - user={selectedCorporateUser} - disabled - disabledFields={{ countryManager: true }} - /> -
- )} - -
- ); - } + const paypalTable = useReactTable({ + data: filteredRows, + columns: paypalColumns, + getCoreRowModel: getCoreRowModel(), + }); + const getUserModal = () => { + if (user) { + if (selectedCorporateUser) { + return ( + setSelectedCorporateUser(undefined)}> + <> + {selectedCorporateUser && ( +
+ { + setSelectedCorporateUser(undefined); + if (shouldReload) reload(); + }} + user={selectedCorporateUser} + disabled + disabledFields={{countryManager: true}} + /> +
+ )} + +
+ ); + } - if (selectedAgentUser) { - return ( - setSelectedAgentUser(undefined)} - > - <> - {selectedAgentUser && ( -
- { - setSelectedAgentUser(undefined); - if (shouldReload) reload(); - }} - user={selectedAgentUser} - /> -
- )} - -
- ); - } - } + if (selectedAgentUser) { + return ( + setSelectedAgentUser(undefined)}> + <> + {selectedAgentUser && ( +
+ { + setSelectedAgentUser(undefined); + if (shouldReload) reload(); + }} + user={selectedAgentUser} + /> +
+ )} + +
+ ); + } + } - return null; - }; + return null; + }; - const getCSVData = () => { - const tables = [table, paypalTable]; - const whitelists = [ - CSV_PAYMENTS_WHITELISTED_KEYS, - CSV_PAYPAL_WHITELISTED_KEYS, - ]; - const currentTable = tables[selectedIndex]; - const whitelist = whitelists[selectedIndex]; - const columns = (currentTable.getHeaderGroups() as any[]).reduce( - (accm: any[], group: any) => { - const whitelistedColumns = group.headers.filter((header: any) => - whitelist.includes(header.id), - ); + const getCSVData = () => { + const tables = [table, paypalTable]; + const whitelists = [CSV_PAYMENTS_WHITELISTED_KEYS, CSV_PAYPAL_WHITELISTED_KEYS]; + const currentTable = tables[selectedIndex]; + const whitelist = whitelists[selectedIndex]; + const columns = (currentTable.getHeaderGroups() as any[]).reduce((accm: any[], group: any) => { + const whitelistedColumns = group.headers.filter((header: any) => whitelist.includes(header.id)); - const data = whitelistedColumns.map((data: any) => ({ - key: data.column.columnDef.id, - label: data.column.columnDef.header, - })) as SimpleCSVColumn[]; + const data = whitelistedColumns.map((data: any) => ({ + key: data.column.columnDef.id, + label: data.column.columnDef.header, + })) as SimpleCSVColumn[]; - return [...accm, ...data]; - }, - [], - ); + return [...accm, ...data]; + }, []); - const { rows } = currentTable.getRowModel(); + const {rows} = currentTable.getRowModel(); - const finalColumns = [ - ...columns, - { - key: "currency", - label: "Currency", - }, - ]; + const finalColumns = [ + ...columns, + { + key: "currency", + label: "Currency", + }, + ]; - return { - columns: finalColumns, - rows: rows.map((row) => { - return finalColumns.reduce((accm, { key }) => { - const { value } = columHelperValue(key, { - row, - getValue: () => row.getValue(key), - }); - return { - ...accm, - [key]: value, - }; - }, {}); - }), - }; - }; + return { + columns: finalColumns, + rows: rows.map((row) => { + return finalColumns.reduce((accm, {key}) => { + const {value} = columHelperValue(key, { + row, + getValue: () => row.getValue(key), + }); + return { + ...accm, + [key]: value, + }; + }, {}); + }), + }; + }; - const { rows: csvRows, columns: csvColumns } = getCSVData(); + const {rows: csvRows, columns: csvColumns} = getCSVData(); - const renderTable = (table: Table) => ( -
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
- ); + const renderTable = (table: Table) => ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); - return ( - <> - - Payment Record | EnCoach - - - - - - {user && ( - - {getUserModal()} - setIsCreatingPayment(false)} - > - setIsCreatingPayment(false)} - reload={reload} - showComission={user.type === "developer" || user.type === "admin"} - /> - + return ( + <> + + Payment Record | EnCoach + + + + + + {user && ( + + {getUserModal()} + setIsCreatingPayment(false)}> + setIsCreatingPayment(false)} + reload={reload} + showComission={user.type === "developer" || user.type === "admin"} + /> + -
-

Payment Record

-
- {(user.type === "developer" || - user.type === "admin" || - user.type === "agent" || - user.type === "corporate") && ( - - )} - {(user.type === "developer" || user.type === "admin") && ( - - )} -
-
- - - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected - ? "bg-white shadow" - : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - } - > - Payments - - {["admin", "developer"].includes(user.type) && ( - - clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-mti-purple-light", - "ring-white ring-opacity-60 ring-offset-2 ring-offset-mti-purple-light focus:outline-none focus:ring-2", - "transition duration-300 ease-in-out", - selected - ? "bg-white shadow" - : "text-blue-100 hover:bg-white/[0.12] hover:text-mti-purple-dark", - ) - } - > - Paypal - - )} - - - -
-
- - u.type === "agent") as AgentUser[] - ).map((user) => ({ - value: user.id, - meta: user, - label: `${user.name} - ${user.email}`, - }))} - value={ - agent - ? { - value: agent?.id, - label: `${agent.name} - ${agent.email}`, - } - : undefined - } - onChange={(value) => - setAgent( - value !== null ? (value as any).meta : undefined, - ) - } - menuPortalTarget={document?.body} - styles={{ - menuPortal: (base) => ({ ...base, zIndex: 9999 }), - control: (styles) => ({ - ...styles, - paddingLeft: "4px", - border: "none", - outline: "none", - ":focus": { - outline: "none", - }, - }), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused - ? "#D5D9F0" - : state.isSelected - ? "#7872BF" - : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - /> -
- )} -
- - e.value === commissionTransfer, - )} - onChange={(value) => { - if (value) return setCommissionTransfer(value.value); - setCommissionTransfer(null); - }} - menuPortalTarget={document?.body} - styles={{ - menuPortal: (base) => ({ ...base, zIndex: 9999 }), - control: (styles) => ({ - ...styles, - paddingLeft: "4px", - border: "none", - outline: "none", - ":focus": { - outline: "none", - }, - }), - option: (styles, state) => ({ - ...styles, - backgroundColor: state.isFocused - ? "#D5D9F0" - : state.isSelected - ? "#7872BF" - : "white", - color: state.isFocused ? "black" : styles.color, - }), - }} - /> -
- )} -
- - u.type === "corporate") as CorporateUser[]).map((user) => ({ + value: user.id, + meta: user, + label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${user.email}`, + }))} + defaultValue={ + user.type === "corporate" + ? { + value: user.id, + meta: user, + label: `${user.corporateInformation?.companyInformation?.name || user.name} - ${ + user.email + }`, + } + : undefined + } + isDisabled={user.type === "corporate"} + onChange={(value) => setCorporate((value as any)?.meta ?? undefined)} + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({...base, zIndex: 9999}), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
+ {user.type !== "corporate" && ( +
+ + e.value === paid)} + onChange={(value) => { + if (value) return setPaid(value.value); + setPaid(null); + }} + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({...base, zIndex: 9999}), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
+
+ + moment(date).isSameOrBefore(moment(new Date()))} + onChange={([initialDate, finalDate]: [Date, Date]) => { + setStartDate(initialDate ?? moment("01/01/2023").toDate()); + if (finalDate) { + // basicly selecting a final day works as if I'm selecting the first + // minute of that day. this way it covers the whole day + setEndDate(moment(finalDate).endOf("day").toDate()); + return; + } + setEndDate(null); + }} + /> +
+ {user.type !== "corporate" && ( +
+ + e.value === corporateTransfer)} + onChange={(value) => { + if (value) return setCorporateTransfer(value.value); + setCorporateTransfer(null); + }} + menuPortalTarget={document?.body} + styles={{ + menuPortal: (base) => ({...base, zIndex: 9999}), + control: (styles) => ({ + ...styles, + paddingLeft: "4px", + border: "none", + outline: "none", + ":focus": { + outline: "none", + }, + }), + option: (styles, state) => ({ + ...styles, + backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", + color: state.isFocused ? "black" : styles.color, + }), + }} + /> +
+
+ {renderTable(table as Table)} +
+ + {renderSearch()} + {renderTable(paypalTable as Table)} + +
+
+
+ )} + + ); } From e13aea9f7de39aae3e9043326a60b6f957da2b84 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 15 May 2024 23:41:45 +0100 Subject: [PATCH 05/12] Updated the table --- src/pages/payment-record.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/payment-record.tsx b/src/pages/payment-record.tsx index 50837f1b..16b16b3c 100644 --- a/src/pages/payment-record.tsx +++ b/src/pages/payment-record.tsx @@ -618,8 +618,7 @@ export default function PaymentRecord() { id: "agentValue", cell: (info) => { const {value} = columHelperValue(info.column.id, info); - const currency = CURRENCIES.find((x) => x.currency === info.row.original.currency)?.label; - const finalValue = `${value} ${currency}`; + const finalValue = `${value} ${info.row.original.currency}`; return {finalValue}; }, }), From a65b72adad0f3003667f4628fb248eb773f43015 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 16 May 2024 13:30:38 +0100 Subject: [PATCH 06/12] Updated the payment integration to be dynamic --- src/components/PaymobPayment.tsx | 2 +- src/pages/api/paymob/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PaymobPayment.tsx b/src/components/PaymobPayment.tsx index ad6d7d62..db34653e 100644 --- a/src/components/PaymobPayment.tsx +++ b/src/components/PaymobPayment.tsx @@ -40,7 +40,7 @@ export default function PaymobPayment({user, price, setIsPaymentLoading, currenc amount: price * 1000, currency: "OMR", items: [], - payment_methods: [1540], + payment_methods: [], customer: { email: user.email, first_name: user.name.split(" ")[0], diff --git a/src/pages/api/paymob/index.ts b/src/pages/api/paymob/index.ts index 58bd7c85..993b188f 100644 --- a/src/pages/api/paymob/index.ts +++ b/src/pages/api/paymob/index.ts @@ -41,7 +41,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const response = await axios.post( "https://oman.paymob.com/v1/intention/", - {...intention, payment_methods: [1540], items: []}, + {...intention, payment_methods: [parseInt(process.env.PAYMOB_INTEGRATION_ID || "0")], items: []}, {headers: {Authorization: `Token ${process.env.PAYMOB_SECRET_KEY}`}}, ); const intentionResult = response.data; From c18afee9add6847b1d665e6a22159bd061a2793f Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 16 May 2024 13:34:18 +0100 Subject: [PATCH 07/12] Updated the packages --- package.json | 1 - yarn.lock | 29 ----------------------------- 2 files changed, 30 deletions(-) diff --git a/package.json b/package.json index 5e91f501..5a6a8479 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "next": "13.1.6", "nodemailer": "^6.9.5", "nodemailer-express-handlebars": "^6.1.0", - "paymob-react": "git+https://github.com/tiago-ecrop/paymob-react-oman.git", "primeicons": "^6.0.1", "primereact": "^9.2.3", "qrcode": "^1.5.3", diff --git a/yarn.lock b/yarn.lock index 043500ba..cfab02ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4870,13 +4870,6 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -"paymob-react@git+https://github.com/tiago-ecrop/paymob-react-oman.git": - version "1.0.0" - resolved "git+https://github.com/tiago-ecrop/paymob-react-oman.git#9e7d1e86f01d29dd10192bbd371517849a264e5d" - dependencies: - react "^18.2.0" - react-dom "^18.2.0" - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" @@ -5208,14 +5201,6 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" -react-dom@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - react-fast-compare@^3.0.1: version "3.2.1" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz" @@ -5345,13 +5330,6 @@ react@18.2.0: dependencies: loose-envify "^1.1.0" -react@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" - read-cache@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" @@ -5557,13 +5535,6 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - seedrandom@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" From d022bd078aa166084cbdc2685d5e0fe3ff023467 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 16 May 2024 13:44:27 +0100 Subject: [PATCH 08/12] Updated the currencies to have OMR as well --- src/pages/(admin)/Lists/PackageList.tsx | 2 +- src/resources/paypal.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/(admin)/Lists/PackageList.tsx b/src/pages/(admin)/Lists/PackageList.tsx index 9c60a28b..780f4657 100644 --- a/src/pages/(admin)/Lists/PackageList.tsx +++ b/src/pages/(admin)/Lists/PackageList.tsx @@ -40,7 +40,7 @@ function PackageCreator({pack, onClose}: {pack?: Package; onClose: () => void}) const [unit, setUnit] = useState(pack?.duration_unit || "months"); const [price, setPrice] = useState(pack?.price || 0); - const [currency, setCurrency] = useState(pack?.currency || "EUR"); + const [currency, setCurrency] = useState(pack?.currency || "OMR"); const submit = () => { (pack ? axios.patch : axios.post)(pack ? `/api/packages/${pack.id}` : "/api/packages", { diff --git a/src/resources/paypal.ts b/src/resources/paypal.ts index 7cd4a838..3f3b7a48 100644 --- a/src/resources/paypal.ts +++ b/src/resources/paypal.ts @@ -95,4 +95,8 @@ export const CURRENCIES: {label: string; currency: string}[] = [ label: "United States dollar", currency: "USD", }, + { + label: "Omani rial", + currency: "OMR", + }, ]; From 2f0cbfe74e2aea62dbf58003133aca97895f90b2 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 16 May 2024 14:30:44 +0100 Subject: [PATCH 09/12] Removed the billing details modal --- src/components/PaymobPayment.tsx | 42 +++++--------------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/src/components/PaymobPayment.tsx b/src/components/PaymobPayment.tsx index db34653e..47cec9d6 100644 --- a/src/components/PaymobPayment.tsx +++ b/src/components/PaymobPayment.tsx @@ -20,15 +20,6 @@ interface Props { export default function PaymobPayment({user, price, setIsPaymentLoading, currency, duration, duration_unit, onSuccess}: Props) { const [isLoading, setIsLoading] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - - const [firstName, setFirstName] = useState(user.name.split(" ")[0]); - const [lastName, setLastName] = useState([...user.name.split(" ")].pop()); - const [street, setStreet] = useState(""); - const [apartment, setApartment] = useState(""); - const [building, setBuilding] = useState(""); - const [state, setState] = useState(""); - const [floor, setFloor] = useState(""); const router = useRouter(); @@ -50,16 +41,16 @@ export default function PaymobPayment({user, price, setIsPaymentLoading, currenc }, }, billing_data: { - apartment: apartment || "N/A", - building: building || "N/A", + apartment: "N/A", + 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", + floor: "N/A", phone_number: user.demographicInformation?.phone || "N/A", - state: state || "N/A", - street: street || "N/A", + state: "N/A", + street: "N/A", }, extras: { userID: user.id, @@ -71,7 +62,6 @@ export default function PaymobPayment({user, price, setIsPaymentLoading, currenc const response = await axios.post<{iframeURL: string}>(`/api/paymob`, paymentIntention); router.push(response.data.iframeURL); - setIsModalOpen(false); } catch (error) { console.error("Error starting card payment process:", error); } @@ -79,27 +69,7 @@ export default function PaymobPayment({user, price, setIsPaymentLoading, currenc return ( <> - setIsModalOpen(false)}> -
-
- - -
-
- - - -
-
- - -
- -
-
- From 649f24e4ae482db894c954f64642cffd3f8aadd9 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 16 May 2024 14:51:19 +0100 Subject: [PATCH 10/12] Updated the showcase --- src/pages/(status)/PaymentDue.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 5a5f00d8..f397bb26 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -7,7 +7,6 @@ import {User} from "@/interfaces/user"; import clsx from "clsx"; import {capitalize} from "lodash"; import {useEffect, useState} from "react"; -import getSymbolFromCurrency from "currency-symbol-map"; import useInvites from "@/hooks/useInvites"; import {BsArrowRepeat} from "react-icons/bs"; import InviteCard from "@/components/Medium/InviteCard"; @@ -124,18 +123,18 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}: {!appliedDiscount && ( {p.price} - {getSymbolFromCurrency(p.currency)} + {p.currency} )} {appliedDiscount && (
{p.price} - {getSymbolFromCurrency(p.currency)} + {p.currency} {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} - {getSymbolFromCurrency(p.currency)} + {p.currency}
)} @@ -178,7 +177,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
{user.corporateInformation.payment.value} - {getSymbolFromCurrency(user.corporateInformation.payment.currency)} + {user.corporateInformation.payment.currency} Date: Thu, 16 May 2024 15:03:31 +0100 Subject: [PATCH 11/12] Added a space to it --- src/pages/(status)/PaymentDue.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index f397bb26..2bf50e4c 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -122,8 +122,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
{!appliedDiscount && ( - {p.price} - {p.currency} + {p.price} {p.currency} )} {appliedDiscount && ( @@ -133,8 +132,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}: {p.currency} - {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} - {p.currency} + {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} {p.currency}
)} @@ -176,8 +174,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}:
- {user.corporateInformation.payment.value} - {user.corporateInformation.payment.currency} + {user.corporateInformation.payment.value} {user.corporateInformation.payment.currency} Date: Thu, 16 May 2024 15:42:13 +0100 Subject: [PATCH 12/12] Added a missing space --- src/pages/(status)/PaymentDue.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/(status)/PaymentDue.tsx b/src/pages/(status)/PaymentDue.tsx index 2bf50e4c..23f1342a 100644 --- a/src/pages/(status)/PaymentDue.tsx +++ b/src/pages/(status)/PaymentDue.tsx @@ -128,8 +128,7 @@ export default function PaymentDue({user, hasExpired = false, clientID, reload}: {appliedDiscount && (
- {p.price} - {p.currency} + {p.price} {p.currency} {(p.price - p.price * (appliedDiscount / 100)).toFixed(2)} {p.currency}