From 1bde4e12c8fcc7524877f33659ea239422852dca Mon Sep 17 00:00:00 2001 From: Suleiman Yunus Date: Mon, 26 Aug 2024 04:24:18 +0300 Subject: [PATCH 1/6] feat(orders): send orders to bc --- .env.example | 6 + csp.js | 23 +- package.json | 3 +- .../(pages)/checkout/CheckoutItem/index.tsx | 29 +- .../checkout/CheckoutPage/index.module.scss | 35 + .../(pages)/checkout/CheckoutPage/index.tsx | 471 ++++++++---- .../OrderConfirmationPage/index.tsx | 2 +- src/payload/bc/endpoints/createSalesHeader.ts | 23 + .../bc/endpoints/createSalesOrderLine.ts | 23 + .../bc/endpoints/fetchCustomersFromBC.ts | 2 +- src/payload/bc/types/MutateSalesOrder.ts | 6 + src/payload/bc/types/MutateSalesOrderLine.ts | 9 + src/payload/bc/types/SalesOrder.ts | 122 ++++ src/payload/bc/types/SalesOrderLine.ts | 96 +++ .../collections/Orders/hooks/syncOrderToBC.ts | 53 ++ src/payload/collections/Orders/index.ts | 76 +- .../collections/Orders/utils/create-order.ts | 55 ++ .../Orders/utils/updateOrderPaymentStatus.ts | 30 + src/payload/collections/Products/index.ts | 2 - src/payload/generated-schema.graphql | 684 ++++++++++++++++-- src/payload/payload-types.ts | 12 +- src/payload/payload.config.ts | 43 +- .../endpoints/getPesapalAccessToken.ts | 33 + .../endpoints/getPesapalTransactionStatus.ts | 25 + src/payload/pesapal/endpoints/ipn.ts | 32 + .../pesapal/endpoints/submitOrderRequest.ts | 25 + src/payload/pesapal/types/mutate-ipn.ts | 5 + .../pesapal/types/mutate-pesapal-order.ts | 27 + .../pesapal/types/pesapal-transaction.ts | 31 + .../pesapal/types/submit-order-request.ts | 7 + yarn.lock | 26 +- 31 files changed, 1762 insertions(+), 254 deletions(-) create mode 100644 src/payload/bc/endpoints/createSalesHeader.ts create mode 100644 src/payload/bc/endpoints/createSalesOrderLine.ts create mode 100644 src/payload/bc/types/MutateSalesOrder.ts create mode 100644 src/payload/bc/types/MutateSalesOrderLine.ts create mode 100644 src/payload/bc/types/SalesOrder.ts create mode 100644 src/payload/bc/types/SalesOrderLine.ts create mode 100644 src/payload/collections/Orders/utils/create-order.ts create mode 100644 src/payload/collections/Orders/utils/updateOrderPaymentStatus.ts create mode 100644 src/payload/pesapal/endpoints/getPesapalAccessToken.ts create mode 100644 src/payload/pesapal/endpoints/getPesapalTransactionStatus.ts create mode 100644 src/payload/pesapal/endpoints/ipn.ts create mode 100644 src/payload/pesapal/endpoints/submitOrderRequest.ts create mode 100644 src/payload/pesapal/types/mutate-ipn.ts create mode 100644 src/payload/pesapal/types/mutate-pesapal-order.ts create mode 100644 src/payload/pesapal/types/pesapal-transaction.ts create mode 100644 src/payload/pesapal/types/submit-order-request.ts diff --git a/.env.example b/.env.example index 13b02a4..8fd4055 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,12 @@ BC_URL= BC_USERNAME= BC_PASSWORD= +# Enable Pesapal integration +PESAPAL_CONSUMER_KEY= +PESAPAL_CONSUMER_SECRET= +PESAPAL_URL= +PESAPAL_NOTIFICATION_ID= + # Enable Stripe integration STRIPE_SECRET_KEY= PAYLOAD_PUBLIC_STRIPE_IS_TEST_KEY=true diff --git a/csp.js b/csp.js index 8e18c04..e789ee0 100644 --- a/csp.js +++ b/csp.js @@ -8,10 +8,23 @@ const policies = { 'https://js.stripe.com', 'https://maps.googleapis.com', 'https://bctest.dayliff.com:7048', + 'https://cybqa.pesapal.com', + 'https://pay.google.com', + 'https://google.com/pay', ], 'child-src': ["'self'"], - 'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], - 'img-src': ["'self'", 'https://*.stripe.com', 'https://raw.githubusercontent.com'], + 'style-src': [ + "'self'", + "'unsafe-inline'", + 'https://fonts.googleapis.com', + 'http://fonts.googleapis.com/css', + ], + 'img-src': [ + "'self'", + 'https://*.stripe.com', + 'https://raw.githubusercontent.com', + 'https://www.gstatic.com/images/icons/material/system/1x/payment_white_36dp.png', + ], 'font-src': ["'self'"], 'frame-src': [ "'self'", @@ -19,6 +32,9 @@ const policies = { 'https://js.stripe.com', 'https://hooks.stripe.com', 'https://bctest.dayliff.com:7048', + 'https://cybqa.pesapal.com', + 'https://pay.google.com', + 'https://google.com/pay', ], 'connect-src': [ "'self'", @@ -26,6 +42,9 @@ const policies = { 'https://api.stripe.com', 'https://maps.googleapis.com', 'https://bctest.dayliff.com:7048', + 'https://cybqa.pesapal.com', + 'https://pay.google.com', + 'https://google.com/pay', ], } diff --git a/package.json b/package.json index 7db932e..f222cc1 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "react-phone-number-input": "^3.4.5", "react-router-dom": "5.3.4", "stripe": "^10.2.0", - "tailwindcss": "^3.4.10" + "tailwindcss": "^3.4.10", + "uuid": "^10.0.0" }, "devDependencies": { "@next/eslint-plugin-next": "^13.1.6", diff --git a/src/app/(pages)/checkout/CheckoutItem/index.tsx b/src/app/(pages)/checkout/CheckoutItem/index.tsx index 7fd2c48..18ae2a6 100644 --- a/src/app/(pages)/checkout/CheckoutItem/index.tsx +++ b/src/app/(pages)/checkout/CheckoutItem/index.tsx @@ -5,9 +5,9 @@ import { Price } from '../../../_components/Price' import classes from './index.module.scss' -export const CheckoutItem = ({ product, title, metaImage, quantity, index }) => { +export const CheckoutItem = ({ product, title, metaImage, quantity }) => { return ( -
  • +
  • {!metaImage && No image} {metaImage && typeof metaImage !== 'string' && ( @@ -15,16 +15,23 @@ export const CheckoutItem = ({ product, title, metaImage, quantity, index }) => )} -
    -
    -
    {title}
    - -
    -

    x{quantity}

    -
    +
    +
    +
    +

    + + {title} + +

    +
    -
    - +
    + +
    +
  • ) diff --git a/src/app/(pages)/checkout/CheckoutPage/index.module.scss b/src/app/(pages)/checkout/CheckoutPage/index.module.scss index 147adaf..b044b3c 100644 --- a/src/app/(pages)/checkout/CheckoutPage/index.module.scss +++ b/src/app/(pages)/checkout/CheckoutPage/index.module.scss @@ -50,3 +50,38 @@ .error { margin-top: calc(var(--block-padding) - var(--base)); } + +.checkboxWrapper { + display: flex; + align-items: center; + gap: 10px; + white-space: nowrap; + cursor: pointer; +} + +.checkbox { + -webkit-appearance: none; + appearance: none; + width: 24px; + height: 24px; + border-radius: 5px; + background-color: white; + border: 2px solid var(--color-dark-60); + outline: none; + cursor: pointer; + } + + .checkbox:checked { + background-color: var(--color-dark-60); + position: relative; + } + + .checkbox:checked::before { + content: '\2713'; + font-size: 14px; + font-weight: bold; + color: #fff; + position: absolute; + right: 5px; + top: 0; + } diff --git a/src/app/(pages)/checkout/CheckoutPage/index.tsx b/src/app/(pages)/checkout/CheckoutPage/index.tsx index fda705d..075b4ce 100644 --- a/src/app/(pages)/checkout/CheckoutPage/index.tsx +++ b/src/app/(pages)/checkout/CheckoutPage/index.tsx @@ -1,25 +1,39 @@ 'use client' -import React, { Fragment, useEffect } from 'react' -import { Elements } from '@stripe/react-stripe-js' -import { loadStripe } from '@stripe/stripe-js' +import React, { Fragment, useCallback, useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' +import GooglePayButton from '@google-pay/button-react' import Link from 'next/link' import { useRouter } from 'next/navigation' +import { v4 as uuidv4 } from 'uuid' -import { Settings } from '../../../../payload/payload-types' +import { createPayloadOrder } from '../../../../payload/collections/Orders/utils/create-order' +import { Order, Settings } from '../../../../payload/payload-types' +import { submitOrderRequest } from '../../../../payload/pesapal/endpoints/submitOrderRequest' import { Button } from '../../../_components/Button' +import { Input } from '../../../_components/Input' import { LoadingShimmer } from '../../../_components/LoadingShimmer' +import { calculatePrice } from '../../../_components/Price' import { useAuth } from '../../../_providers/Auth' import { useCart } from '../../../_providers/Cart' -import { useTheme } from '../../../_providers/Theme' -import cssVariables from '../../../cssVariables' -import { CheckoutForm } from '../CheckoutForm' import { CheckoutItem } from '../CheckoutItem' import classes from './index.module.scss' -const apiKey = `${process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}` -const stripe = loadStripe(apiKey) +type FormData = { + toShipName: string + toShipCompany: string + toShipAddress: string + toShipApartment: string + toShipcity: string + toShipCountry: string + toShipPhone: string + toBillName: string + toBillPhone: string +} + +const CALLBACK_URL = process.env.NEXT_PUBLIC_SERVER_URL +const PESAPAL_NOTIFICATION_ID = process.env.NEXT_PUBLIC_PESAPAL_NOTIFICATION_ID export const CheckoutPage: React.FC<{ settings: Settings @@ -31,50 +45,111 @@ export const CheckoutPage: React.FC<{ const { user } = useAuth() const router = useRouter() const [error, setError] = React.useState(null) - const [clientSecret, setClientSecret] = React.useState() - const hasMadePaymentIntent = React.useRef(false) - const { theme } = useTheme() + const [hideBilling, setHideBilling] = useState(true) + const [loading, setLoading] = useState(false) const { cart, cartIsEmpty, cartTotal } = useCart() - useEffect(() => { - if (user !== null && cartIsEmpty) { - router.push('/cart') - } - }, [router, user, cartIsEmpty]) + const { + register, + handleSubmit, + formState: { errors }, + watch, + setValue, + } = useForm() - useEffect(() => { - if (user && cart && hasMadePaymentIntent.current === false) { - hasMadePaymentIntent.current = true + const onSubmit = useCallback( + async (data: FormData) => { + setLoading(true) + const orderReference = uuidv4() + const currency = 'KES' + const paymentMethod = 'pesapal' + + const payload = { + id: orderReference, + currency: currency, + amount: cartTotal?.raw, + description: `Order for ${user?.name}`, + callback_url: `${CALLBACK_URL}/order-confirmation`, + notification_id: PESAPAL_NOTIFICATION_ID, + billing_address: { + name: data.toBillName, + phone_number: data.toBillPhone, + }, + } + + const response = await submitOrderRequest(payload) + + if (response.status === '200') { + const { redirect_url, order_tracking_id } = response + const pesaPalDetails = { + orderTrackingId: order_tracking_id, + } - const makeIntent = async () => { try { - const paymentReq = await fetch( - `${process.env.NEXT_PUBLIC_SERVER_URL}/api/create-payment-intent`, - { - method: 'POST', - credentials: 'include', + console.log('trying to create') + const orderReq = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/orders`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', }, - ) - - const res = await paymentReq.json() - - if (res.error) { - setError(res.error) - } else if (res.client_secret) { - setError(null) - setClientSecret(res.client_secret) - } - } catch (e) { - setError('Something went wrong.') + body: JSON.stringify({ + orderReference, + currency, + paymentMethod, + pesapalDetails: { + OrderTrackingId: pesaPalDetails, + }, + googlePayDetails: {}, + total: cartTotal.raw, + items: (cart?.items || [])?.map(({ product, quantity }) => ({ + product: typeof product === 'string' ? product : product.id, + quantity, + price: + typeof product === 'object' + ? calculatePrice(product.unitPrice, 1, true) + : undefined, + })), + }), + }) + + console.log('sent request') + + if (!orderReq.ok) throw new Error(orderReq.statusText || 'Something went wrong.') + } catch (err: unknown) { + setLoading(false) + setError(`We could'nt create your order`) + throw new Error(`We couldn't create your order`) } + + window.location.href = redirect_url + } else { + setLoading(false) } + }, + [cart, cartTotal, user?.name], + ) + + const handleToggleBilling = () => { + setHideBilling(!hideBilling) - makeIntent() + if (hideBilling) { + setValue('toBillName', watch('toShipName')) + setValue('toBillPhone', watch('toShipPhone')) + } else { + setValue('toBillName', '') + setValue('toBillPhone', '') } - }, [cart, user]) + } - if (!user || !stripe) return null + useEffect(() => { + if (user !== null && cartIsEmpty) { + router.push('/cart') + } + }, [router, user, cartIsEmpty]) + + if (!user) return null return ( @@ -92,94 +167,240 @@ export const CheckoutPage: React.FC<{ )} {!cartIsEmpty && ( -
    -
    -

    Products

    -
    -

    -

    Quantity

    -
    -

    Subtotal

    -
    +
    +

    Checkout

    + +
    +
    +
    + { + console.log('load payment data', paymentRequest) + }} + /> +
    + +
    + + +
    +

    Shipping information

    -
      - {cart?.items?.map((item, index) => { - if (typeof item.product === 'object') { - const { - quantity, - product, - product: { title, meta }, - } = item - - if (!quantity) return null - - const metaImage = meta?.image - - return ( - - + + + + + + + + +
      + - - ) - } - return null - })} -
      -

      Order Total

      -

      {cartTotal.formatted}

      + + +
      + + +
      + +

      Billing information

      + +
      + +
      + + {!hideBilling && ( +
      + + + +
      + )} + +
    - -
    - )} - {!clientSecret && !error && ( -
    - -
    - )} - {!clientSecret && error && ( -
    -

    {`Error: ${error}`}

    -
    + +
    +

    Order summary

    + +
    +
      + {cart?.items?.map(item => { + if (typeof item.product === 'object') { + const { + quantity, + product, + product: { title, meta }, + } = item + + if (!quantity) return null + + const metaImage = meta?.image + + return ( + + ) + } + return null + })} +
    +
    + +
    +
    +
    Subtotal
    +
    $104.00
    +
    +
    +
    Taxes
    +
    $8.32
    +
    +
    +
    Shipping
    +
    $14.00
    +
    +
    +
    Total
    +
    $126.32
    +
    +
    +
    +
    +
    )} - { - -

    Payment Details

    - {error &&

    {`Error: ${error}`}

    } - - - -
    - } ) } diff --git a/src/app/(pages)/order-confirmation/OrderConfirmationPage/index.tsx b/src/app/(pages)/order-confirmation/OrderConfirmationPage/index.tsx index 11be89c..ec604fd 100644 --- a/src/app/(pages)/order-confirmation/OrderConfirmationPage/index.tsx +++ b/src/app/(pages)/order-confirmation/OrderConfirmationPage/index.tsx @@ -11,7 +11,7 @@ import classes from './index.module.scss' export const OrderConfirmationPage: React.FC<{}> = () => { const searchParams = useSearchParams() - const orderID = searchParams.get('order_id') + const orderID = searchParams.get('orderTrackingId') const error = searchParams.get('error') const { clearCart } = useCart() diff --git a/src/payload/bc/endpoints/createSalesHeader.ts b/src/payload/bc/endpoints/createSalesHeader.ts new file mode 100644 index 0000000..b95186f --- /dev/null +++ b/src/payload/bc/endpoints/createSalesHeader.ts @@ -0,0 +1,23 @@ +import type { MutateSalesOrder } from "../types/MutateSalesOrder" +import type { SalesOrder } from "../types/SalesOrder" + +const BC_URL = process.env.BC_URL +const USERNAME = process.env.BC_USERNAME +const PASSWORD = process.env.BC_PASSWORD + +export async function createSalesHeader(payload: MutateSalesOrder): Promise { + const postSalesOrder = await fetch(`${BC_URL}/Sales_Order`, { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Basic ' + Buffer.from(USERNAME + ':' + PASSWORD).toString('base64'), + }, + method: 'POST', + body: JSON.stringify(payload), + }) + + if (!postSalesOrder.ok) { + throw new Error(`${postSalesOrder.status}`) + } + + return await postSalesOrder.json() +} diff --git a/src/payload/bc/endpoints/createSalesOrderLine.ts b/src/payload/bc/endpoints/createSalesOrderLine.ts new file mode 100644 index 0000000..0672902 --- /dev/null +++ b/src/payload/bc/endpoints/createSalesOrderLine.ts @@ -0,0 +1,23 @@ +import type { MutateSalesOrderLine } from "../types/MutateSalesOrderLine" +import type { SalesOrderLine } from "../types/SalesOrderLine" + +const BC_URL = process.env.BC_URL +const USERNAME = process.env.BC_USERNAME +const PASSWORD = process.env.BC_PASSWORD + +export async function createSalesOrderLine(payload: MutateSalesOrderLine): Promise { + const postSalesOrderLine = await fetch(`${BC_URL}/Sales_Order_Line`, { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Basic ' + Buffer.from(USERNAME + ':' + PASSWORD).toString('base64'), + }, + method: 'POST', + body: JSON.stringify(payload), + }) + + if (!postSalesOrderLine.ok) { + throw new Error(`${postSalesOrderLine.status}`) + } + + return await postSalesOrderLine.json() +} diff --git a/src/payload/bc/endpoints/fetchCustomersFromBC.ts b/src/payload/bc/endpoints/fetchCustomersFromBC.ts index 3136138..35f029e 100644 --- a/src/payload/bc/endpoints/fetchCustomersFromBC.ts +++ b/src/payload/bc/endpoints/fetchCustomersFromBC.ts @@ -4,7 +4,7 @@ const BC_URL = process.env.BC_URL const USERNAME = process.env.BC_USERNAME const PASSWORD = process.env.BC_PASSWORD; -export async function fetchCustomersFromBC({ limit = 50, filters }: { limit?: number; filters?: Record }): Promise { +export async function fetchCustomersFromBC({ limit = 50, filters }: { limit?: number; filters?: Partial> }): Promise { let filterString: string | undefined; let filter: string | undefined; diff --git a/src/payload/bc/types/MutateSalesOrder.ts b/src/payload/bc/types/MutateSalesOrder.ts new file mode 100644 index 0000000..7eaa451 --- /dev/null +++ b/src/payload/bc/types/MutateSalesOrder.ts @@ -0,0 +1,6 @@ +export interface MutateSalesOrder { + Document_Type: "Order"; + No?: string + Sell_to_Customer_No: string; + Location_Code?: string; +} diff --git a/src/payload/bc/types/MutateSalesOrderLine.ts b/src/payload/bc/types/MutateSalesOrderLine.ts new file mode 100644 index 0000000..f61522b --- /dev/null +++ b/src/payload/bc/types/MutateSalesOrderLine.ts @@ -0,0 +1,9 @@ +export interface MutateSalesOrderLine { + Type: string; + Document_No: string; + No: string; + Location_Code?: string; + ShortcutDimCode4: string; + Quantity: number; + Invoice_Discount_Percent?: number; +} diff --git a/src/payload/bc/types/SalesOrder.ts b/src/payload/bc/types/SalesOrder.ts new file mode 100644 index 0000000..d568015 --- /dev/null +++ b/src/payload/bc/types/SalesOrder.ts @@ -0,0 +1,122 @@ +export interface SalesOrder { + "@odata.context": string; + "@odata.etag": string; + Document_Type: string; + No: string; + Sell_to_Customer_No: string; + Sell_to_Customer_Name: string; + Quote_No: string; + Posting_Description: string; + Retail_Type: string; + Sell_to_Address: string; + Sell_to_Address_2: string; + Sell_to_City: string; + Sell_to_County: string; + Sell_to_Post_Code: string; + Sell_to_Country_Region_Code: string; + Sell_to_Contact_No: string; + Sell_to_Phone_No: string; + Sell_to_E_Mail: string; + Sell_to_Contact: string; + No_of_Archived_Versions: number; + Document_Date: Date; + Posting_Date: Date; + Order_Date: Date; + Due_Date: Date; + Promised_Delivery_Date: Date; + External_Document_No: string; + Your_Reference: string; + Govt_LPO_No: string; + Salesperson_Code: string; + Web_UserId: string; + Shortcut_Dimension_1_Code: string; + ShortCutDim1Name: string; + Shortcut_Dimension_2_Code: string; + ShortCutDim2Name: string; + Amount_To_Pay: number; + Campaign_No: string; + Opportunity_No: string; + Responsibility_Center: string; + Assigned_User_ID: string; + Job_Queue_Status: string; + Status: string; + Credit_Status_TMN: string; + Exit_Point: string; + Logistics_To_Deliver: boolean; + ShiptoName: string; + ShiptoContactNo: string; + Ship_to_Address2: string; + Contact_Name: string; + Contact_Phone_No: string; + Physical_Address: string; + Physical_Address_2: string; + City: string; + Delivery_Instruction: string; + Released_By: string; + Release_DateTime: Date; + Requested_Delivery_Date: Date; + WorkDescription: string; + Committment_Type: string; + Receipt_No: string; + LPO_Amount: number; + Approver_ID: string; + Reviewer_Status: string; + Reviewer_ID: string; + Reviewed_Date: Date; + Review_Comments: string; + Enable_LPO_Authorization: boolean; + Currency_Code: string; + Prices_Including_VAT: boolean; + VAT_Bus_Posting_Group: string; + Payment_Terms_Code: string; + Amount_Including_VAT: number; + Channel_Type: string; + Payment_Method_Code: string; + EU_3_Party_Trade: boolean; + SelectedPayments: string; + Payment_Discount_Percent: number; + Pmt_Discount_Date: Date; + Direct_Debit_Mandate_ID: string; + Applies_to_Doc_Type: string; + Applies_to_Doc_No: string; + ShippingOptions: string; + Ship_to_Code: string; + Ship_to_Address_2: string; + Ship_to_City: string; + Ship_to_County: string; + Ship_to_Post_Code: string; + Ship_to_Country_Region_Code: string; + Shipment_Method_Code: string; + Shipping_Agent_Code: string; + Shipping_Agent_Service_Code: string; + Package_Tracking_No: string; + BillToOptions: string; + Bill_to_Name: string; + Bill_to_Address: string; + Bill_to_Address_2: string; + Bill_to_City: string; + Bill_to_County: string; + Bill_to_Post_Code: string; + Bill_to_Country_Region_Code: string; + Bill_to_Contact_No: string; + Bill_to_Contact: string; + Location_Code: string; + Shipment_Date: Date; + Shipping_Advice: string; + Outbound_Whse_Handling_Time: string; + Shipping_Time: string; + Shipping_No: string; + Late_Order_Shipping: boolean; + Combine_Shipments: boolean; + Transaction_Specification: string; + Transaction_Type: string; + Transport_Method: string; + Area: string; + Prepayment_Percent: number; + Compress_Prepayment: boolean; + Prepmt_Payment_Terms_Code: string; + Prepayment_Due_Date: Date; + Prepmt_Payment_Discount_Percent: number; + Prepmt_Pmt_Discount_Date: Date; + Date_Filter: string; +} diff --git a/src/payload/bc/types/SalesOrderLine.ts b/src/payload/bc/types/SalesOrderLine.ts new file mode 100644 index 0000000..ca6bd07 --- /dev/null +++ b/src/payload/bc/types/SalesOrderLine.ts @@ -0,0 +1,96 @@ +export interface SalesOrderLine { + "@odata.context": string; + "@odata.etag": string; + Document_Type: string; + Document_No: string; + Line_No: number; + Type: string; + FilteredTypeField: string; + No: string; + Cross_Reference_No: string; + IC_Partner_Code: string; + IC_Partner_Ref_Type: string; + IC_Partner_Reference: string; + Variant_Code: string; + Substitution_Available: boolean; + Purchasing_Code: string; + Nonstock: boolean; + VAT_Bus_Posting_Group: string; + Description: string; + Drop_Shipment: boolean; + Special_Order: boolean; + Return_Reason_Code: string; + Location_Code: string; + Bin_Code: string; + Control50: string; + Quantity: number; + Qty_to_Assemble_to_Order: number; + Reserved_Quantity: number; + Unit_of_Measure_Code: string; + Unit_of_Measure: string; + Unit_Cost_LCY: number; + SalesPriceExist: boolean; + Unit_Price: number; + Tax_Liable: boolean; + Tax_Area_Code: string; + Tax_Group_Code: string; + Line_Discount_Percent: number; + VAT_Prod_Posting_Group: string; + Line_Amount: number; + SalesLineDiscExists: boolean; + Line_Discount_Amount: number; + Prepayment_Percent: number; + Prepmt_Line_Amount: number; + Prepmt_Amt_Inv: number; + Allow_Invoice_Disc: boolean; + Inv_Discount_Amount: number; + Inv_Disc_Amount_to_Invoice: number; + Qty_to_Ship: number; + Quantity_Shipped: number; + Qty_to_Invoice: number; + Quantity_Invoiced: number; + Prepmt_Amt_to_Deduct: number; + Prepmt_Amt_Deducted: number; + Allow_Item_Charge_Assignment: boolean; + Qty_to_Assign: number; + Qty_Assigned: number; + Requested_Delivery_Date: Date; + Promised_Delivery_Date: Date; + Planned_Delivery_Date: Date; + Planned_Shipment_Date: Date; + Shipment_Date: Date; + Shipping_Agent_Code: string; + Shipping_Agent_Service_Code: string; + Shipping_Time: string; + Work_Type_Code: string; + Whse_Outstanding_Qty: number; + Whse_Outstanding_Qty_Base: number; + ATO_Whse_Outstanding_Qty: number; + ATO_Whse_Outstd_Qty_Base: number; + Outbound_Whse_Handling_Time: string; + Blanket_Order_No: string; + Blanket_Order_Line_No: number; + FA_Posting_Date: Date; + Depr_until_FA_Posting_Date: boolean; + Depreciation_Book_Code: string; + Use_Duplication_List: boolean; + Duplicate_in_Depreciation_Book: string; + Appl_from_Item_Entry: number; + Appl_to_Item_Entry: number; + Deferral_Code: string; + Shortcut_Dimension_1_Code: string; + Shortcut_Dimension_2_Code: string; + ShortcutDimCode3: string; + ShortcutDimCode4: string; + ShortcutDimCode5: string; + ShortcutDimCode6: string; + ShortcutDimCode7: string; + ShortcutDimCode8: string; + TotalSalesLine_Line_Amount: number; + Invoice_Discount_Amount: number; + Invoice_Disc_Pct: number; + Invoice_Discount_Percent: number; + Total_Amount_Excl_VAT: number; + Total_VAT_Amount: number; + Total_Amount_Incl_VAT: number; +} diff --git a/src/payload/collections/Orders/hooks/syncOrderToBC.ts b/src/payload/collections/Orders/hooks/syncOrderToBC.ts index e69de29..01811ed 100644 --- a/src/payload/collections/Orders/hooks/syncOrderToBC.ts +++ b/src/payload/collections/Orders/hooks/syncOrderToBC.ts @@ -0,0 +1,53 @@ +import type { AfterChangeHook } from 'payload/dist/collections/config/types' + +import { createSalesHeader } from '../../../bc/endpoints/createSalesHeader' +import { createSalesOrderLine } from '../../../bc/endpoints/createSalesOrderLine' +import type { Order } from '../../../payload-types' + +export const syncOrderToBC: AfterChangeHook = async ({ doc, req, operation }) => { + const { payload } = req + + if ((operation === 'update') && doc.items && doc.status === 'paid') { + const orderedBy = typeof doc.orderedBy === 'object' ? doc.orderedBy.id : doc.orderedBy + + const user = await payload.findByID({ + collection: 'users', + id: orderedBy, + }) + + if (user) { + throw new Error(`Couldn't find a user with orderReference ${doc.orderReference}`) + } + + const salesOrderBody = { + Document_Type: 'Order' as const, + Sell_to_Customer_No: user.bcCustomerID, + } + + const salesOrder = await createSalesHeader(salesOrderBody) + + if (!salesOrder) { + throw new Error(`Could't create a sales order for order with reference ${doc.orderReference}`) + } + + for (const item of doc.items) { + const bcProductID = typeof item.product === 'object' ? item.product.bcProductID : item.product + + const orderLineBody = { + Type: "Item" as const, + Document_No: salesOrder.No, + No: bcProductID, + ShortcutDimCode4: "41040", + Quantity: item.quantity, + } + + const salesOrderLine = await createSalesOrderLine(orderLineBody) + + if (!salesOrderLine) { + throw new Error(`Couldn't create a sales order line for item ${item.id}`) + } + } + } + + return +} diff --git a/src/payload/collections/Orders/index.ts b/src/payload/collections/Orders/index.ts index 28f28b1..ce9ff30 100644 --- a/src/payload/collections/Orders/index.ts +++ b/src/payload/collections/Orders/index.ts @@ -5,19 +5,19 @@ import { adminsOrLoggedIn } from '../../access/adminsOrLoggedIn' import { adminsOrOrderedBy } from './access/adminsOrOrderedBy' import { clearUserCart } from './hooks/clearUserCart' import { populateOrderedBy } from './hooks/populateOrderedBy' +import { syncOrderToBC } from './hooks/syncOrderToBC' import { updateUserPurchases } from './hooks/updateUserPurchases' -import { LinkToPaymentIntent } from './ui/LinkToPaymentIntent' export const Orders: CollectionConfig = { slug: 'orders', admin: { useAsTitle: 'createdAt', - defaultColumns: ['createdAt', 'orderedBy'], + defaultColumns: ['createdAt', 'orderedBy', 'status'], preview: doc => `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/orders/${doc.id}`, }, hooks: { beforeChange: [], - afterChange: [updateUserPurchases, clearUserCart], + afterChange: [updateUserPurchases, clearUserCart, syncOrderToBC], }, access: { read: adminsOrOrderedBy, @@ -26,6 +26,11 @@ export const Orders: CollectionConfig = { delete: admins, }, fields: [ + { + name: 'orderReference', + type: 'text', + required: true, + }, { name: 'orderedBy', type: 'relationship', @@ -34,23 +39,66 @@ export const Orders: CollectionConfig = { beforeChange: [populateOrderedBy], }, }, - { - name: 'stripePaymentIntentID', - label: 'Stripe Payment Intent ID', - type: 'text', - admin: { - position: 'sidebar', - components: { - Field: LinkToPaymentIntent, - }, - }, - }, { name: 'total', type: 'number', required: true, min: 0, }, + { + name: 'currency', + type: 'select', + options: [ + { label: 'KES', value: 'KES' }, + { label: 'USD', value: 'USD' }, + ], + defaultValue: 'KES', + required: true, + }, + { + name: 'status', + type: 'select', + options: [ + { label: 'Pending', value: 'pending' }, + { label: 'Paid', value: 'paid' }, + { label: 'Failed', value: 'failed' }, + ], + defaultValue: 'pending', + required: true, + }, + { + name: 'paymentMethod', + type: 'select', + options: [ + { label: 'PesaPal', value: 'pesapal' }, + { label: 'Google Pay', value: 'googlepay' }, + ], + required: true, + }, + { + name: 'pesapalDetails', + type: 'group', + fields: [ + { + name: 'orderTrackingId', + type: 'text', + }, + ], + }, + { + name: 'googlePayDetails', + type: 'group', + fields: [ + { + name: 'transactionId', + type: 'text', + }, + { + name: 'paymentMethodToken', + type: 'text', + }, + ], + }, { name: 'items', type: 'array', diff --git a/src/payload/collections/Orders/utils/create-order.ts b/src/payload/collections/Orders/utils/create-order.ts new file mode 100644 index 0000000..ae0afd9 --- /dev/null +++ b/src/payload/collections/Orders/utils/create-order.ts @@ -0,0 +1,55 @@ +import type { Order, User } from "../../../payload-types" + +interface CreatPayloadOrder { + cartTotal: { + formatted: string + raw: number + } + cart: User['cart']; + orderReference: string; + currency?: string; + paymentMethod: string; + pesaPalDetails?: { orderTrackingId: string }; + googlePayDetails?: string; +} + +export async function createPayloadOrder({ cartTotal, cart, orderReference, currency, paymentMethod, pesaPalDetails, googlePayDetails }: CreatPayloadOrder): Promise { + try { + const orderReq = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/orders`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + orderReference, + currency, + paymentMethod, + pesapalDetails: { + OrderTrackingId: pesaPalDetails, + }, + googlePayDetails: {}, + total: cartTotal.raw, + items: (cart?.items || [])?.map(({ product, quantity }) => ({ + product: typeof product === 'string' ? product : product.id, + quantity, + price: 1, + })), + }), + }) + + if (!orderReq.ok) throw new Error(orderReq.statusText || 'Something went wrong.') + + const { + error: errorFromRes, + }: { + message?: string + error?: string + doc: Order + } = await orderReq.json() + + if (errorFromRes) throw new Error(errorFromRes) + } catch (err: unknown) { + throw new Error(`We couldn't create your order`) + } +} diff --git a/src/payload/collections/Orders/utils/updateOrderPaymentStatus.ts b/src/payload/collections/Orders/utils/updateOrderPaymentStatus.ts new file mode 100644 index 0000000..5687ed1 --- /dev/null +++ b/src/payload/collections/Orders/utils/updateOrderPaymentStatus.ts @@ -0,0 +1,30 @@ +import payload from "payload" +export async function updateOrderPaymentStatus(orderReference: string, paymentStatus: 'pending' | 'failed' | 'paid'): Promise { + try { + const order = await payload.find({ + collection: 'orders', + where: { + orderReference: { + equals: orderReference, + }, + }, + }) + + if (order.docs.length < 1) { + throw new Error(`Order with orderReference ${orderReference} could not be found`) + } + + await payload.update({ + collection: 'orders', + id: order.docs[0].id, + data: { + status: paymentStatus, + }, + }) + + + } catch (err: unknown) { + payload.logger.error(err) + throw new Error(`We couldn't update your order with orderReference ${orderReference}`) + } +} diff --git a/src/payload/collections/Products/index.ts b/src/payload/collections/Products/index.ts index cfda160..0d1e9ff 100644 --- a/src/payload/collections/Products/index.ts +++ b/src/payload/collections/Products/index.ts @@ -8,7 +8,6 @@ import { MediaBlock } from '../../blocks/MediaBlock' import { slugField } from '../../fields/slug' import { populateArchiveBlock } from '../../hooks/populateArchiveBlock' import { checkUserPurchases } from './access/checkUserPurchases' -import { beforeProductChange } from './hooks/beforeChange' import { deleteProductFromCarts } from './hooks/deleteProductFromCarts' import { revalidateProduct } from './hooks/revalidateProduct' import { ProductSelect } from './ui/ProductSelect' @@ -25,7 +24,6 @@ const Products: CollectionConfig = { }, }, hooks: { - beforeChange: [beforeProductChange], afterChange: [revalidateProduct], afterRead: [populateArchiveBlock], afterDelete: [deleteProductFromCarts], diff --git a/src/payload/generated-schema.graphql b/src/payload/generated-schema.graphql index 53722b2..265014d 100644 --- a/src/payload/generated-schema.graphql +++ b/src/payload/generated-schema.graphql @@ -4982,9 +4982,14 @@ input versionsProduct_where_or { type Order { id: String + orderReference: String! orderedBy: User - stripePaymentIntentID: String total: Float! + currency: Order_currency! + status: Order_status! + paymentMethod: Order_paymentMethod! + pesapalDetails: Order_PesapalDetails + googlePayDetails: Order_GooglePayDetails items: [Order_Items!] updatedAt: DateTime createdAt: DateTime @@ -5032,6 +5037,31 @@ A field whose value conforms to the standard internet email address format as sp """ scalar EmailAddress @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address") +enum Order_currency { + KES + USD +} + +enum Order_status { + pending + paid + failed +} + +enum Order_paymentMethod { + pesapal + googlepay +} + +type Order_PesapalDetails { + orderTrackingId: String +} + +type Order_GooglePayDetails { + transactionId: String + paymentMethodToken: String +} + type Order_Items { product: Product price: Float @@ -5054,9 +5084,15 @@ type Orders { } input Order_where { + orderReference: Order_orderReference_operator orderedBy: Order_orderedBy_operator - stripePaymentIntentID: Order_stripePaymentIntentID_operator total: Order_total_operator + currency: Order_currency_operator + status: Order_status_operator + paymentMethod: Order_paymentMethod_operator + pesapalDetails__orderTrackingId: Order_pesapalDetails__orderTrackingId_operator + googlePayDetails__transactionId: Order_googlePayDetails__transactionId_operator + googlePayDetails__paymentMethodToken: Order_googlePayDetails__paymentMethodToken_operator items__product: Order_items__product_operator items__price: Order_items__price_operator items__quantity: Order_items__quantity_operator @@ -5068,6 +5104,16 @@ input Order_where { OR: [Order_where_or] } +input Order_orderReference_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] +} + input Order_orderedBy_operator { equals: JSON not_equals: JSON @@ -5077,7 +5123,56 @@ input Order_orderedBy_operator { exists: Boolean } -input Order_stripePaymentIntentID_operator { +input Order_total_operator { + equals: Float + not_equals: Float + greater_than_equal: Float + greater_than: Float + less_than_equal: Float + less_than: Float +} + +input Order_currency_operator { + equals: Order_currency_Input + not_equals: Order_currency_Input + in: [Order_currency_Input] + not_in: [Order_currency_Input] + all: [Order_currency_Input] +} + +enum Order_currency_Input { + KES + USD +} + +input Order_status_operator { + equals: Order_status_Input + not_equals: Order_status_Input + in: [Order_status_Input] + not_in: [Order_status_Input] + all: [Order_status_Input] +} + +enum Order_status_Input { + pending + paid + failed +} + +input Order_paymentMethod_operator { + equals: Order_paymentMethod_Input + not_equals: Order_paymentMethod_Input + in: [Order_paymentMethod_Input] + not_in: [Order_paymentMethod_Input] + all: [Order_paymentMethod_Input] +} + +enum Order_paymentMethod_Input { + pesapal + googlepay +} + +input Order_pesapalDetails__orderTrackingId_operator { equals: String not_equals: String like: String @@ -5088,13 +5183,26 @@ input Order_stripePaymentIntentID_operator { exists: Boolean } -input Order_total_operator { - equals: Float - not_equals: Float - greater_than_equal: Float - greater_than: Float - less_than_equal: Float - less_than: Float +input Order_googlePayDetails__transactionId_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean +} + +input Order_googlePayDetails__paymentMethodToken_operator { + equals: String + not_equals: String + like: String + contains: String + in: [String] + not_in: [String] + all: [String] + exists: Boolean } input Order_items__product_operator { @@ -5170,9 +5278,15 @@ input Order_id_operator { } input Order_where_and { + orderReference: Order_orderReference_operator orderedBy: Order_orderedBy_operator - stripePaymentIntentID: Order_stripePaymentIntentID_operator total: Order_total_operator + currency: Order_currency_operator + status: Order_status_operator + paymentMethod: Order_paymentMethod_operator + pesapalDetails__orderTrackingId: Order_pesapalDetails__orderTrackingId_operator + googlePayDetails__transactionId: Order_googlePayDetails__transactionId_operator + googlePayDetails__paymentMethodToken: Order_googlePayDetails__paymentMethodToken_operator items__product: Order_items__product_operator items__price: Order_items__price_operator items__quantity: Order_items__quantity_operator @@ -5185,9 +5299,15 @@ input Order_where_and { } input Order_where_or { + orderReference: Order_orderReference_operator orderedBy: Order_orderedBy_operator - stripePaymentIntentID: Order_stripePaymentIntentID_operator total: Order_total_operator + currency: Order_currency_operator + status: Order_status_operator + paymentMethod: Order_paymentMethod_operator + pesapalDetails__orderTrackingId: Order_pesapalDetails__orderTrackingId_operator + googlePayDetails__transactionId: Order_googlePayDetails__transactionId_operator + googlePayDetails__paymentMethodToken: Order_googlePayDetails__paymentMethodToken_operator items__product: Order_items__product_operator items__price: Order_items__price_operator items__quantity: Order_items__quantity_operator @@ -5208,57 +5328,62 @@ type ordersDocAccess { } type OrdersDocAccessFields { + orderReference: OrdersDocAccessFields_orderReference orderedBy: OrdersDocAccessFields_orderedBy - stripePaymentIntentID: OrdersDocAccessFields_stripePaymentIntentID total: OrdersDocAccessFields_total + currency: OrdersDocAccessFields_currency + status: OrdersDocAccessFields_status + paymentMethod: OrdersDocAccessFields_paymentMethod + pesapalDetails: OrdersDocAccessFields_pesapalDetails + googlePayDetails: OrdersDocAccessFields_googlePayDetails items: OrdersDocAccessFields_items updatedAt: OrdersDocAccessFields_updatedAt createdAt: OrdersDocAccessFields_createdAt } -type OrdersDocAccessFields_orderedBy { - create: OrdersDocAccessFields_orderedBy_Create - read: OrdersDocAccessFields_orderedBy_Read - update: OrdersDocAccessFields_orderedBy_Update - delete: OrdersDocAccessFields_orderedBy_Delete +type OrdersDocAccessFields_orderReference { + create: OrdersDocAccessFields_orderReference_Create + read: OrdersDocAccessFields_orderReference_Read + update: OrdersDocAccessFields_orderReference_Update + delete: OrdersDocAccessFields_orderReference_Delete } -type OrdersDocAccessFields_orderedBy_Create { +type OrdersDocAccessFields_orderReference_Create { permission: Boolean! } -type OrdersDocAccessFields_orderedBy_Read { +type OrdersDocAccessFields_orderReference_Read { permission: Boolean! } -type OrdersDocAccessFields_orderedBy_Update { +type OrdersDocAccessFields_orderReference_Update { permission: Boolean! } -type OrdersDocAccessFields_orderedBy_Delete { +type OrdersDocAccessFields_orderReference_Delete { permission: Boolean! } -type OrdersDocAccessFields_stripePaymentIntentID { - create: OrdersDocAccessFields_stripePaymentIntentID_Create - read: OrdersDocAccessFields_stripePaymentIntentID_Read - update: OrdersDocAccessFields_stripePaymentIntentID_Update - delete: OrdersDocAccessFields_stripePaymentIntentID_Delete +type OrdersDocAccessFields_orderedBy { + create: OrdersDocAccessFields_orderedBy_Create + read: OrdersDocAccessFields_orderedBy_Read + update: OrdersDocAccessFields_orderedBy_Update + delete: OrdersDocAccessFields_orderedBy_Delete } -type OrdersDocAccessFields_stripePaymentIntentID_Create { +type OrdersDocAccessFields_orderedBy_Create { permission: Boolean! } -type OrdersDocAccessFields_stripePaymentIntentID_Read { +type OrdersDocAccessFields_orderedBy_Read { permission: Boolean! } -type OrdersDocAccessFields_stripePaymentIntentID_Update { +type OrdersDocAccessFields_orderedBy_Update { permission: Boolean! } -type OrdersDocAccessFields_stripePaymentIntentID_Delete { +type OrdersDocAccessFields_orderedBy_Delete { permission: Boolean! } @@ -5285,6 +5410,201 @@ type OrdersDocAccessFields_total_Delete { permission: Boolean! } +type OrdersDocAccessFields_currency { + create: OrdersDocAccessFields_currency_Create + read: OrdersDocAccessFields_currency_Read + update: OrdersDocAccessFields_currency_Update + delete: OrdersDocAccessFields_currency_Delete +} + +type OrdersDocAccessFields_currency_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_currency_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_currency_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_currency_Delete { + permission: Boolean! +} + +type OrdersDocAccessFields_status { + create: OrdersDocAccessFields_status_Create + read: OrdersDocAccessFields_status_Read + update: OrdersDocAccessFields_status_Update + delete: OrdersDocAccessFields_status_Delete +} + +type OrdersDocAccessFields_status_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_status_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_status_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_status_Delete { + permission: Boolean! +} + +type OrdersDocAccessFields_paymentMethod { + create: OrdersDocAccessFields_paymentMethod_Create + read: OrdersDocAccessFields_paymentMethod_Read + update: OrdersDocAccessFields_paymentMethod_Update + delete: OrdersDocAccessFields_paymentMethod_Delete +} + +type OrdersDocAccessFields_paymentMethod_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_paymentMethod_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_paymentMethod_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_paymentMethod_Delete { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails { + create: OrdersDocAccessFields_pesapalDetails_Create + read: OrdersDocAccessFields_pesapalDetails_Read + update: OrdersDocAccessFields_pesapalDetails_Update + delete: OrdersDocAccessFields_pesapalDetails_Delete + fields: OrdersDocAccessFields_pesapalDetails_Fields +} + +type OrdersDocAccessFields_pesapalDetails_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails_Delete { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails_Fields { + orderTrackingId: OrdersDocAccessFields_pesapalDetails_orderTrackingId +} + +type OrdersDocAccessFields_pesapalDetails_orderTrackingId { + create: OrdersDocAccessFields_pesapalDetails_orderTrackingId_Create + read: OrdersDocAccessFields_pesapalDetails_orderTrackingId_Read + update: OrdersDocAccessFields_pesapalDetails_orderTrackingId_Update + delete: OrdersDocAccessFields_pesapalDetails_orderTrackingId_Delete +} + +type OrdersDocAccessFields_pesapalDetails_orderTrackingId_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails_orderTrackingId_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails_orderTrackingId_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_pesapalDetails_orderTrackingId_Delete { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails { + create: OrdersDocAccessFields_googlePayDetails_Create + read: OrdersDocAccessFields_googlePayDetails_Read + update: OrdersDocAccessFields_googlePayDetails_Update + delete: OrdersDocAccessFields_googlePayDetails_Delete + fields: OrdersDocAccessFields_googlePayDetails_Fields +} + +type OrdersDocAccessFields_googlePayDetails_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_Delete { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_Fields { + transactionId: OrdersDocAccessFields_googlePayDetails_transactionId + paymentMethodToken: OrdersDocAccessFields_googlePayDetails_paymentMethodToken +} + +type OrdersDocAccessFields_googlePayDetails_transactionId { + create: OrdersDocAccessFields_googlePayDetails_transactionId_Create + read: OrdersDocAccessFields_googlePayDetails_transactionId_Read + update: OrdersDocAccessFields_googlePayDetails_transactionId_Update + delete: OrdersDocAccessFields_googlePayDetails_transactionId_Delete +} + +type OrdersDocAccessFields_googlePayDetails_transactionId_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_transactionId_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_transactionId_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_transactionId_Delete { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_paymentMethodToken { + create: OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Create + read: OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Read + update: OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Update + delete: OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Delete +} + +type OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Create { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Read { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Update { + permission: Boolean! +} + +type OrdersDocAccessFields_googlePayDetails_paymentMethodToken_Delete { + permission: Boolean! +} + type OrdersDocAccessFields_items { create: OrdersDocAccessFields_items_Create read: OrdersDocAccessFields_items_Read @@ -10676,57 +10996,62 @@ type ordersAccess { } type OrdersFields { + orderReference: OrdersFields_orderReference orderedBy: OrdersFields_orderedBy - stripePaymentIntentID: OrdersFields_stripePaymentIntentID total: OrdersFields_total + currency: OrdersFields_currency + status: OrdersFields_status + paymentMethod: OrdersFields_paymentMethod + pesapalDetails: OrdersFields_pesapalDetails + googlePayDetails: OrdersFields_googlePayDetails items: OrdersFields_items updatedAt: OrdersFields_updatedAt createdAt: OrdersFields_createdAt } -type OrdersFields_orderedBy { - create: OrdersFields_orderedBy_Create - read: OrdersFields_orderedBy_Read - update: OrdersFields_orderedBy_Update - delete: OrdersFields_orderedBy_Delete +type OrdersFields_orderReference { + create: OrdersFields_orderReference_Create + read: OrdersFields_orderReference_Read + update: OrdersFields_orderReference_Update + delete: OrdersFields_orderReference_Delete } -type OrdersFields_orderedBy_Create { +type OrdersFields_orderReference_Create { permission: Boolean! } -type OrdersFields_orderedBy_Read { +type OrdersFields_orderReference_Read { permission: Boolean! } -type OrdersFields_orderedBy_Update { +type OrdersFields_orderReference_Update { permission: Boolean! } -type OrdersFields_orderedBy_Delete { +type OrdersFields_orderReference_Delete { permission: Boolean! } -type OrdersFields_stripePaymentIntentID { - create: OrdersFields_stripePaymentIntentID_Create - read: OrdersFields_stripePaymentIntentID_Read - update: OrdersFields_stripePaymentIntentID_Update - delete: OrdersFields_stripePaymentIntentID_Delete +type OrdersFields_orderedBy { + create: OrdersFields_orderedBy_Create + read: OrdersFields_orderedBy_Read + update: OrdersFields_orderedBy_Update + delete: OrdersFields_orderedBy_Delete } -type OrdersFields_stripePaymentIntentID_Create { +type OrdersFields_orderedBy_Create { permission: Boolean! } -type OrdersFields_stripePaymentIntentID_Read { +type OrdersFields_orderedBy_Read { permission: Boolean! } -type OrdersFields_stripePaymentIntentID_Update { +type OrdersFields_orderedBy_Update { permission: Boolean! } -type OrdersFields_stripePaymentIntentID_Delete { +type OrdersFields_orderedBy_Delete { permission: Boolean! } @@ -10753,6 +11078,201 @@ type OrdersFields_total_Delete { permission: Boolean! } +type OrdersFields_currency { + create: OrdersFields_currency_Create + read: OrdersFields_currency_Read + update: OrdersFields_currency_Update + delete: OrdersFields_currency_Delete +} + +type OrdersFields_currency_Create { + permission: Boolean! +} + +type OrdersFields_currency_Read { + permission: Boolean! +} + +type OrdersFields_currency_Update { + permission: Boolean! +} + +type OrdersFields_currency_Delete { + permission: Boolean! +} + +type OrdersFields_status { + create: OrdersFields_status_Create + read: OrdersFields_status_Read + update: OrdersFields_status_Update + delete: OrdersFields_status_Delete +} + +type OrdersFields_status_Create { + permission: Boolean! +} + +type OrdersFields_status_Read { + permission: Boolean! +} + +type OrdersFields_status_Update { + permission: Boolean! +} + +type OrdersFields_status_Delete { + permission: Boolean! +} + +type OrdersFields_paymentMethod { + create: OrdersFields_paymentMethod_Create + read: OrdersFields_paymentMethod_Read + update: OrdersFields_paymentMethod_Update + delete: OrdersFields_paymentMethod_Delete +} + +type OrdersFields_paymentMethod_Create { + permission: Boolean! +} + +type OrdersFields_paymentMethod_Read { + permission: Boolean! +} + +type OrdersFields_paymentMethod_Update { + permission: Boolean! +} + +type OrdersFields_paymentMethod_Delete { + permission: Boolean! +} + +type OrdersFields_pesapalDetails { + create: OrdersFields_pesapalDetails_Create + read: OrdersFields_pesapalDetails_Read + update: OrdersFields_pesapalDetails_Update + delete: OrdersFields_pesapalDetails_Delete + fields: OrdersFields_pesapalDetails_Fields +} + +type OrdersFields_pesapalDetails_Create { + permission: Boolean! +} + +type OrdersFields_pesapalDetails_Read { + permission: Boolean! +} + +type OrdersFields_pesapalDetails_Update { + permission: Boolean! +} + +type OrdersFields_pesapalDetails_Delete { + permission: Boolean! +} + +type OrdersFields_pesapalDetails_Fields { + orderTrackingId: OrdersFields_pesapalDetails_orderTrackingId +} + +type OrdersFields_pesapalDetails_orderTrackingId { + create: OrdersFields_pesapalDetails_orderTrackingId_Create + read: OrdersFields_pesapalDetails_orderTrackingId_Read + update: OrdersFields_pesapalDetails_orderTrackingId_Update + delete: OrdersFields_pesapalDetails_orderTrackingId_Delete +} + +type OrdersFields_pesapalDetails_orderTrackingId_Create { + permission: Boolean! +} + +type OrdersFields_pesapalDetails_orderTrackingId_Read { + permission: Boolean! +} + +type OrdersFields_pesapalDetails_orderTrackingId_Update { + permission: Boolean! +} + +type OrdersFields_pesapalDetails_orderTrackingId_Delete { + permission: Boolean! +} + +type OrdersFields_googlePayDetails { + create: OrdersFields_googlePayDetails_Create + read: OrdersFields_googlePayDetails_Read + update: OrdersFields_googlePayDetails_Update + delete: OrdersFields_googlePayDetails_Delete + fields: OrdersFields_googlePayDetails_Fields +} + +type OrdersFields_googlePayDetails_Create { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_Read { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_Update { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_Delete { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_Fields { + transactionId: OrdersFields_googlePayDetails_transactionId + paymentMethodToken: OrdersFields_googlePayDetails_paymentMethodToken +} + +type OrdersFields_googlePayDetails_transactionId { + create: OrdersFields_googlePayDetails_transactionId_Create + read: OrdersFields_googlePayDetails_transactionId_Read + update: OrdersFields_googlePayDetails_transactionId_Update + delete: OrdersFields_googlePayDetails_transactionId_Delete +} + +type OrdersFields_googlePayDetails_transactionId_Create { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_transactionId_Read { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_transactionId_Update { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_transactionId_Delete { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_paymentMethodToken { + create: OrdersFields_googlePayDetails_paymentMethodToken_Create + read: OrdersFields_googlePayDetails_paymentMethodToken_Read + update: OrdersFields_googlePayDetails_paymentMethodToken_Update + delete: OrdersFields_googlePayDetails_paymentMethodToken_Delete +} + +type OrdersFields_googlePayDetails_paymentMethodToken_Create { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_paymentMethodToken_Read { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_paymentMethodToken_Update { + permission: Boolean! +} + +type OrdersFields_googlePayDetails_paymentMethodToken_Delete { + permission: Boolean! +} + type OrdersFields_items { create: OrdersFields_items_Create read: OrdersFields_items_Read @@ -13564,14 +14084,44 @@ enum ProductUpdate__status_MutationInput { } input mutationOrderInput { + orderReference: String! orderedBy: String - stripePaymentIntentID: String total: Float! + currency: Order_currency_MutationInput! + status: Order_status_MutationInput! + paymentMethod: Order_paymentMethod_MutationInput! + pesapalDetails: mutationOrder_PesapalDetailsInput + googlePayDetails: mutationOrder_GooglePayDetailsInput items: [mutationOrder_ItemsInput] updatedAt: String createdAt: String } +enum Order_currency_MutationInput { + KES + USD +} + +enum Order_status_MutationInput { + pending + paid + failed +} + +enum Order_paymentMethod_MutationInput { + pesapal + googlepay +} + +input mutationOrder_PesapalDetailsInput { + orderTrackingId: String +} + +input mutationOrder_GooglePayDetailsInput { + transactionId: String + paymentMethodToken: String +} + input mutationOrder_ItemsInput { product: String price: Float @@ -13580,14 +14130,44 @@ input mutationOrder_ItemsInput { } input mutationOrderUpdateInput { + orderReference: String orderedBy: String - stripePaymentIntentID: String total: Float + currency: OrderUpdate_currency_MutationInput + status: OrderUpdate_status_MutationInput + paymentMethod: OrderUpdate_paymentMethod_MutationInput + pesapalDetails: mutationOrderUpdate_PesapalDetailsInput + googlePayDetails: mutationOrderUpdate_GooglePayDetailsInput items: [mutationOrderUpdate_ItemsInput] updatedAt: String createdAt: String } +enum OrderUpdate_currency_MutationInput { + KES + USD +} + +enum OrderUpdate_status_MutationInput { + pending + paid + failed +} + +enum OrderUpdate_paymentMethod_MutationInput { + pesapal + googlepay +} + +input mutationOrderUpdate_PesapalDetailsInput { + orderTrackingId: String +} + +input mutationOrderUpdate_GooglePayDetailsInput { + transactionId: String + paymentMethodToken: String +} + input mutationOrderUpdate_ItemsInput { product: String price: Float diff --git a/src/payload/payload-types.ts b/src/payload/payload-types.ts index b5ace07..3649ab9 100644 --- a/src/payload/payload-types.ts +++ b/src/payload/payload-types.ts @@ -402,9 +402,19 @@ export interface Product { } export interface Order { id: string; + orderReference: string; orderedBy?: (string | null) | User; - stripePaymentIntentID?: string | null; total: number; + currency: 'KES' | 'USD'; + status: 'pending' | 'paid' | 'failed'; + paymentMethod: 'pesapal' | 'googlepay'; + pesapalDetails?: { + orderTrackingId?: string | null; + }; + googlePayDetails?: { + transactionId?: string | null; + paymentMethodToken?: string | null; + }; items?: | { product: string | Product; diff --git a/src/payload/payload.config.ts b/src/payload/payload.config.ts index bd89524..40f90b7 100644 --- a/src/payload/payload.config.ts +++ b/src/payload/payload.config.ts @@ -6,7 +6,6 @@ import redirects from '@payloadcms/plugin-redirects' import search from '@payloadcms/plugin-search' import seo from '@payloadcms/plugin-seo' import type { GenerateTitle } from '@payloadcms/plugin-seo/types' -import stripePlugin from '@payloadcms/plugin-stripe' import { slateEditor } from '@payloadcms/richtext-slate' // editor-import import dotenv from 'dotenv' import path from 'path' @@ -21,15 +20,11 @@ import Products from './collections/Products' import Users from './collections/Users' import BeforeDashboard from './components/BeforeDashboard' import BeforeLogin from './components/BeforeLogin' -import { createPaymentIntent } from './endpoints/create-payment-intent' -import { customersProxy } from './endpoints/customers' -import { productsProxy } from './endpoints/products' import { seed } from './endpoints/seed' import { Footer } from './globals/Footer' import { Header } from './globals/Header' import { Settings } from './globals/Settings' -import { priceUpdated } from './stripe/webhooks/priceUpdated' -import { productUpdated } from './stripe/webhooks/productUpdated' +import ipn from './pesapal/endpoints/ipn' const generateTitle: GenerateTitle = () => { return 'My Store' @@ -64,15 +59,12 @@ export default buildConfig({ alias: { ...config.resolve?.alias, dotenv: path.resolve(__dirname, './dotenv.js'), - [path.resolve(__dirname, 'collections/Products/hooks/beforeChange')]: mockModulePath, - [path.resolve(__dirname, 'collections/Users/hooks/createStripeCustomer')]: - mockModulePath, - [path.resolve(__dirname, 'collections/Users/endpoints/customer')]: mockModulePath, - [path.resolve(__dirname, 'endpoints/create-payment-intent')]: mockModulePath, - [path.resolve(__dirname, 'endpoints/customers')]: mockModulePath, [path.resolve(__dirname, 'endpoints/products')]: mockModulePath, [path.resolve(__dirname, 'endpoints/seed')]: mockModulePath, - stripe: mockModulePath, + [path.resolve(__dirname, 'pesapal/endpoints/ipn')]: mockModulePath, + [path.resolve(__dirname, 'pesapal/endpoints/getPesapalAccessToken')]: mockModulePath, + [path.resolve(__dirname, 'pesapal/endpoints/getPesapalTransactionStatus')]: mockModulePath, + [path.resolve(__dirname, 'pesapal/endpoints/submitOrderRequest')]: mockModulePath, express: mockModulePath, }, }, @@ -102,19 +94,9 @@ export default buildConfig({ ), endpoints: [ { - path: '/create-payment-intent', + path: '/ipn', method: 'post', - handler: createPaymentIntent, - }, - { - path: '/stripe/customers', - method: 'get', - handler: customersProxy, - }, - { - path: '/stripe/products', - method: 'get', - handler: productsProxy, + handler: ipn, }, // The seed endpoint is used to populate the database with some example data // You should delete this endpoint before deploying your site to production @@ -125,17 +107,6 @@ export default buildConfig({ }, ], plugins: [ - stripePlugin({ - stripeSecretKey: process.env.STRIPE_SECRET_KEY || '', - isTestKey: Boolean(process.env.PAYLOAD_PUBLIC_STRIPE_IS_TEST_KEY), - stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET, - rest: false, - webhooks: { - 'product.created': productUpdated, - 'product.updated': productUpdated, - 'price.updated': priceUpdated, - }, - }), redirects({ collections: ['pages', 'products'], }), diff --git a/src/payload/pesapal/endpoints/getPesapalAccessToken.ts b/src/payload/pesapal/endpoints/getPesapalAccessToken.ts new file mode 100644 index 0000000..2190ef7 --- /dev/null +++ b/src/payload/pesapal/endpoints/getPesapalAccessToken.ts @@ -0,0 +1,33 @@ +const PESAPAL_URL = process.env.NEXT_PUBLIC_PESAPAL_URL +const PESAPAL_CONSUMER_KEY = process.env.NEXT_PUBLIC_PESAPAL_CONSUMER_KEY +const PESAPAL_CONSUMER_SECRET = process.env.NEXT_PUBLIC_PESAPAL_CONSUMER_SECRET + +interface PesapalAuthToken { + token: string; + expiryDate: string; + error: null, + status: string; + message: string; +} + +const payload = { + consumer_key: PESAPAL_CONSUMER_KEY, + consumer_secret: PESAPAL_CONSUMER_SECRET, +} + +export async function getPesapalAccessToken(): Promise { + const response = await fetch(`${PESAPAL_URL}/Auth/RequestToken`, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify(payload), + }) + + if (!response.ok) { + throw new Error(`${response.status}`) + } + + return await response.json() +} diff --git a/src/payload/pesapal/endpoints/getPesapalTransactionStatus.ts b/src/payload/pesapal/endpoints/getPesapalTransactionStatus.ts new file mode 100644 index 0000000..45efd7c --- /dev/null +++ b/src/payload/pesapal/endpoints/getPesapalTransactionStatus.ts @@ -0,0 +1,25 @@ +import type { PesapalTransactionStatus } from "../types/pesapal-transaction" +import { getPesapalAccessToken } from "./getPesapalAccessToken" + +const PESAPAL_URL = process.env.NEXT_PUBLIC_PESAPAL_URL + +export async function getPesapalTransactionStatus(orderTrackingId: string): Promise { + const authToken = await getPesapalAccessToken() + + const response = await fetch( + `${PESAPAL_URL}/api/Transactions/GetTransactionStatus?orderTrackingId=${orderTrackingId}`, + { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${authToken.token}`, + }, + } + ); + + if (!response.ok) { + throw new Error(`${response.status}`); + } + + return await response.json(); +} diff --git a/src/payload/pesapal/endpoints/ipn.ts b/src/payload/pesapal/endpoints/ipn.ts new file mode 100644 index 0000000..b88171c --- /dev/null +++ b/src/payload/pesapal/endpoints/ipn.ts @@ -0,0 +1,32 @@ +import type { PayloadHandler } from 'payload/config'; + +import { updateOrderPaymentStatus } from '../../collections/Orders/utils/updateOrderPaymentStatus'; +import { getPesapalTransactionStatus } from './getPesapalTransactionStatus'; + +const ipn: PayloadHandler = async (req, res): Promise => { + if (req.method === 'POST') { + const { OrderTrackingId, OrderMerchantReference, OrderNotificationType } = req.body; + + if (!OrderTrackingId) { + throw new Error('No OrderTracking ID found') + } + + const transactionStatus = await getPesapalTransactionStatus(OrderTrackingId) + + switch (transactionStatus.description) { + case 'COMPLETED': + await updateOrderPaymentStatus(OrderMerchantReference, 'paid'); + break; + case 'FAILED': + await updateOrderPaymentStatus(OrderMerchantReference, 'failed'); + break; + } + + res.status(200).send({ "orderNotificationType": OrderNotificationType, "orderTrackingId": OrderTrackingId, "orderMerchantReference": OrderMerchantReference, "status": 200 }); + } else { + res.setHeader('Allow', ['POST']); + res.status(405).send(`Method ${req.method} Not Allowed`); + } +}; + +export default ipn; diff --git a/src/payload/pesapal/endpoints/submitOrderRequest.ts b/src/payload/pesapal/endpoints/submitOrderRequest.ts new file mode 100644 index 0000000..cf3d869 --- /dev/null +++ b/src/payload/pesapal/endpoints/submitOrderRequest.ts @@ -0,0 +1,25 @@ +import type { MutatePesapal } from "../types/mutate-pesapal-order" +import type { SubmitOrderResquest } from "../types/submit-order-request" +import { getPesapalAccessToken } from "./getPesapalAccessToken" + +const PESAPAL_URL = process.env.NEXT_PUBLIC_PESAPAL_URL + +export async function submitOrderRequest(payload: MutatePesapal): Promise { + const authToken = await getPesapalAccessToken() + + const postOrderRequest = await fetch(`${PESAPAL_URL}/Transactions/SubmitOrderRequest`, { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${authToken.token}`, + }, + method: 'POST', + body: JSON.stringify(payload), + }) + + if (!postOrderRequest.ok) { + throw new Error(`${postOrderRequest.status}`) + } + + return await postOrderRequest.json() +} diff --git a/src/payload/pesapal/types/mutate-ipn.ts b/src/payload/pesapal/types/mutate-ipn.ts new file mode 100644 index 0000000..6c5ca8a --- /dev/null +++ b/src/payload/pesapal/types/mutate-ipn.ts @@ -0,0 +1,5 @@ +export interface MutateIPN { + OrderNotificationType?: string; + OrderTrackingId: string; + OrderMerchantReference?: string; +} diff --git a/src/payload/pesapal/types/mutate-pesapal-order.ts b/src/payload/pesapal/types/mutate-pesapal-order.ts new file mode 100644 index 0000000..f32bc8d --- /dev/null +++ b/src/payload/pesapal/types/mutate-pesapal-order.ts @@ -0,0 +1,27 @@ +export interface MutatePesapal { + id: string; + currency: string; + amount: number; + description: string; + callback_url: string; + redirect_mode?: string; + cancellation_url?: string; + notification_id: string; + branch?: string; + billing_address: BillingAddress; +} + +export interface BillingAddress { + email_address?: string; + phone_number: string; + country_code?: string; + first_name?: string; + middle_name?: string; + last_name?: string; + line_1?: string; + line_2?: string; + city?: string; + state?: string; + postal_code?: string; + zip_code?: string; +} diff --git a/src/payload/pesapal/types/pesapal-transaction.ts b/src/payload/pesapal/types/pesapal-transaction.ts new file mode 100644 index 0000000..6659060 --- /dev/null +++ b/src/payload/pesapal/types/pesapal-transaction.ts @@ -0,0 +1,31 @@ +export interface PesapalTransactionStatus { + payment_method: string; + amount: number; + created_date: Date; + confirmation_code: string; + payment_status_description: (keyof typeof PaymentStatus); + description: string; + message: string; + payment_account: string; + call_back_url: string; + status_code: (typeof PaymentStatus)[keyof typeof PaymentStatus]; + merchant_reference: string; + payment_status_code: string; + currency: string; + error: Error; + status: string; +} + +export interface Error { + error_type: null; + code: null; + message: null; + call_back_url: null; +} + +export const PaymentStatus = { + INVALID: 0, + COMPLETED: 1, + FAILED: 2, + REVERSED: 3, +} as const diff --git a/src/payload/pesapal/types/submit-order-request.ts b/src/payload/pesapal/types/submit-order-request.ts new file mode 100644 index 0000000..c211edc --- /dev/null +++ b/src/payload/pesapal/types/submit-order-request.ts @@ -0,0 +1,7 @@ +export interface SubmitOrderResquest { + order_tracking_id: string; + merchant_reference: string; + redirect_url: string; + error: number | null; + status: string; +} diff --git a/yarn.lock b/yarn.lock index 10b4033..38facda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1459,7 +1459,7 @@ "@next/env@13.5.2": version "13.5.2" - resolved "https://registry.npmjs.org/@next/env/-/env-13.5.2.tgz" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.2.tgz#1c09e6cf1df8b1edf3cf0ca9c0e0119a49802a5d" integrity sha512-dUseBIQVax+XtdJPzhwww4GetTjlkRSsXeQnisIJWBaHsnxYcN2RGzsPHi58D6qnkATjnhuAtQTJmR1hKYQQPg== "@next/eslint-plugin-next@^13.1.6": @@ -1471,7 +1471,7 @@ "@next/swc-darwin-arm64@13.5.2": version "13.5.2" - resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.2.tgz" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.2.tgz#f099a36fdd06b1949eb4e190aee95a52b97d3885" integrity sha512-7eAyunAWq6yFwdSQliWMmGhObPpHTesiKxMw4DWVxhm5yLotBj8FCR4PXGkpRP2tf8QhaWuVba+/fyAYggqfQg== "@next/swc-darwin-x64@13.5.2": @@ -2319,7 +2319,7 @@ "@swc/helpers@0.5.2": version "0.5.2" - resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== dependencies: tslib "^2.4.0" @@ -3383,11 +3383,16 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001651: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001651: version "1.0.30001651" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== +caniuse-lite@^1.0.30001406: + version "1.0.30001653" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz#b8af452f8f33b1c77f122780a4aecebea0caca56" + integrity sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw== + chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" @@ -6609,7 +6614,7 @@ next-tick@1, next-tick@^1.1.0: next@13.5.2: version "13.5.2" - resolved "https://registry.npmjs.org/next/-/next-13.5.2.tgz" + resolved "https://registry.yarnpkg.com/next/-/next-13.5.2.tgz#809dd84e481049e298fe79d28b1d66b587483fca" integrity sha512-vog4UhUaMYAzeqfiAAmgB/QWLW7p01/sg+2vn6bqc/CxHFYizMzLv6gjxKzl31EVFkfl/F+GbxlKizlkTE9RdA== dependencies: "@next/env" "13.5.2" @@ -7766,7 +7771,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ postcss@8.4.14: version "8.4.14" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== dependencies: nanoid "^3.3.4" @@ -9511,6 +9516,11 @@ uuid@9.0.1, uuid@^9.0.0: resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" @@ -9540,7 +9550,7 @@ warning@^4.0.2: watchpack@2.4.0: version "2.4.0" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== dependencies: glob-to-regexp "^0.4.1" @@ -9845,5 +9855,5 @@ yocto-queue@^0.1.0: zod@3.21.4: version "3.21.4" - resolved "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== From 3a45143829c3127fbe3d12ef67fba9ce4c574b40 Mon Sep 17 00:00:00 2001 From: Suleiman Yunus Date: Mon, 26 Aug 2024 18:06:17 +0300 Subject: [PATCH 2/6] feat(payment): complete payment with pesapal --- package.json | 1 + public/no-image-available.jpg | Bin 0 -> 9741 bytes src/app/(pages)/cart/CartPage/index.tsx | 8 - .../(pages)/checkout/CheckoutPage/index.tsx | 137 ++++++++---------- src/app/(pages)/checkout/page.tsx | 6 +- .../CreateAccountForm/index.tsx | 8 +- src/app/(pages)/not-found.tsx | 10 +- .../OrderConfirmationPage/index.tsx | 2 +- src/app/(pages)/orders/[id]/page.tsx | 16 +- src/app/_components/AddToCartButton/index.tsx | 65 ++------- src/app/_components/Header/Nav/index.tsx | 41 ++++-- src/app/_components/Price/index.tsx | 2 +- src/payload/access/adminsOrLoggedIn.ts | 2 + .../collections/Orders/hooks/clearUserCart.ts | 2 +- .../collections/Orders/hooks/syncOrderToBC.ts | 8 +- src/payload/collections/Orders/index.ts | 9 +- .../collections/Orders/utils/create-order.ts | 15 +- .../Orders/utils/updateOrderPaymentStatus.ts | 20 ++- src/payload/generated-schema.graphql | 80 ++-------- src/payload/payload-types.ts | 3 +- .../endpoints/getPesapalTransactionStatus.ts | 2 +- src/payload/pesapal/endpoints/ipn.ts | 17 ++- yarn.lock | 126 ++++++++++++++++ 23 files changed, 302 insertions(+), 278 deletions(-) create mode 100644 public/no-image-available.jpg diff --git a/package.json b/package.json index f222cc1..c60b108 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@payloadcms/plugin-seo": "^1.0.10", "@payloadcms/plugin-stripe": "^0.0.19", "@payloadcms/richtext-slate": "^1.0.0", + "@radix-ui/react-navigation-menu": "^1.2.0", "@stripe/react-stripe-js": "^1.16.3", "@stripe/stripe-js": "^1.46.0", "axios": "^1.7.4", diff --git a/public/no-image-available.jpg b/public/no-image-available.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bfc8e361b1c744cc92f4379b9a16b8d59707068b GIT binary patch literal 9741 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
    {cartTotal.formatted}
    -
    -
    Shipping
    -
    250.00
    -
    -
    -
    Tax
    -
    120.32
    -
    Order total
    diff --git a/src/app/(pages)/checkout/CheckoutPage/index.tsx b/src/app/(pages)/checkout/CheckoutPage/index.tsx index 075b4ce..489a649 100644 --- a/src/app/(pages)/checkout/CheckoutPage/index.tsx +++ b/src/app/(pages)/checkout/CheckoutPage/index.tsx @@ -5,9 +5,7 @@ import { useForm } from 'react-hook-form' import GooglePayButton from '@google-pay/button-react' import Link from 'next/link' import { useRouter } from 'next/navigation' -import { v4 as uuidv4 } from 'uuid' -import { createPayloadOrder } from '../../../../payload/collections/Orders/utils/create-order' import { Order, Settings } from '../../../../payload/payload-types' import { submitOrderRequest } from '../../../../payload/pesapal/endpoints/submitOrderRequest' import { Button } from '../../../_components/Button' @@ -37,10 +35,9 @@ const PESAPAL_NOTIFICATION_ID = process.env.NEXT_PUBLIC_PESAPAL_NOTIFICATION_ID export const CheckoutPage: React.FC<{ settings: Settings -}> = props => { - const { - settings: { productsPage }, - } = props + token: string +}> = ({ settings, token }) => { + const { productsPage } = settings const { user } = useAuth() const router = useRouter() @@ -61,74 +58,74 @@ export const CheckoutPage: React.FC<{ const onSubmit = useCallback( async (data: FormData) => { setLoading(true) - const orderReference = uuidv4() + const currency = 'KES' const paymentMethod = 'pesapal' - const payload = { - id: orderReference, - currency: currency, - amount: cartTotal?.raw, - description: `Order for ${user?.name}`, - callback_url: `${CALLBACK_URL}/order-confirmation`, - notification_id: PESAPAL_NOTIFICATION_ID, - billing_address: { - name: data.toBillName, - phone_number: data.toBillPhone, - }, - } + try { + const orderReq = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/orders`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `JWT ${token}`, + }, + body: JSON.stringify({ + currency, + paymentMethod, + total: cartTotal.raw, + items: (cart?.items || [])?.map(({ product, quantity }) => ({ + product: typeof product === 'string' ? product : product.id, + quantity, + price: + typeof product === 'object' + ? calculatePrice(product.unitPrice, quantity, true) + : undefined, + })), + }), + }) + + if (!orderReq.ok) throw new Error(orderReq.statusText || 'Something went wrong.') + + const { + error: errorFromRes, + doc, + }: { + message?: string + error?: string + doc: Order + } = await orderReq.json() + + if (errorFromRes) throw new Error(errorFromRes) + + const payload = { + id: doc.id, + currency: currency, + amount: cartTotal?.raw, + description: `Order for ${user?.name}`, + callback_url: `${CALLBACK_URL}/order-confirmation`, + notification_id: PESAPAL_NOTIFICATION_ID, + billing_address: { + name: data.toBillName, + phone_number: data.toBillPhone, + }, + } - const response = await submitOrderRequest(payload) + const response = await submitOrderRequest(payload) - if (response.status === '200') { - const { redirect_url, order_tracking_id } = response - const pesaPalDetails = { - orderTrackingId: order_tracking_id, - } + if (response.status === '200') { + const { redirect_url } = response - try { - console.log('trying to create') - const orderReq = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/orders`, { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - orderReference, - currency, - paymentMethod, - pesapalDetails: { - OrderTrackingId: pesaPalDetails, - }, - googlePayDetails: {}, - total: cartTotal.raw, - items: (cart?.items || [])?.map(({ product, quantity }) => ({ - product: typeof product === 'string' ? product : product.id, - quantity, - price: - typeof product === 'object' - ? calculatePrice(product.unitPrice, 1, true) - : undefined, - })), - }), - }) - - console.log('sent request') - - if (!orderReq.ok) throw new Error(orderReq.statusText || 'Something went wrong.') - } catch (err: unknown) { + window.location.href = redirect_url + } else { + setError(response.status) setLoading(false) - setError(`We could'nt create your order`) - throw new Error(`We couldn't create your order`) + throw new Error(`Couldn't process your payment`) } - - window.location.href = redirect_url - } else { - setLoading(false) + } catch (err: unknown) { + throw new Error(`We couldn't create your order`) } }, - [cart, cartTotal, user?.name], + [cart, cartTotal, user], ) const handleToggleBilling = () => { @@ -382,19 +379,11 @@ export const CheckoutPage: React.FC<{
    Subtotal
    -
    $104.00
    -
    -
    -
    Taxes
    -
    $8.32
    -
    -
    -
    Shipping
    -
    $14.00
    +
    {cartTotal.formatted}
    Total
    -
    $126.32
    +
    {cartTotal.formatted}
    diff --git a/src/app/(pages)/checkout/page.tsx b/src/app/(pages)/checkout/page.tsx index 0bad886..07e187d 100644 --- a/src/app/(pages)/checkout/page.tsx +++ b/src/app/(pages)/checkout/page.tsx @@ -11,12 +11,14 @@ import { CheckoutPage } from './CheckoutPage' import classes from './index.module.scss' export default async function Checkout() { - await getMeUser({ + const me = await getMeUser({ nullUserRedirect: `/login?error=${encodeURIComponent( 'You must be logged in to checkout.', )}&redirect=${encodeURIComponent('/checkout')}`, }) + const token = me.token + let settings: Settings | null = null try { @@ -29,7 +31,7 @@ export default async function Checkout() { return (
    - +
    ) diff --git a/src/app/(pages)/create-account/CreateAccountForm/index.tsx b/src/app/(pages)/create-account/CreateAccountForm/index.tsx index f3a4833..1444be2 100644 --- a/src/app/(pages)/create-account/CreateAccountForm/index.tsx +++ b/src/app/(pages)/create-account/CreateAccountForm/index.tsx @@ -59,6 +59,7 @@ const CreateAccountForm: React.FC = () => { const onSubmit = useCallback( async (data: FormData) => { + setLoading(true) const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/users`, { method: 'POST', body: JSON.stringify({ ...data, phoneNumber: data.phoneNumber.replace('+', '') }), @@ -75,17 +76,12 @@ const CreateAccountForm: React.FC = () => { const redirect = searchParams.get('redirect') - const timer = setTimeout(() => { - setLoading(true) - }, 1000) - try { await login(data) - clearTimeout(timer) if (redirect) router.push(redirect as string) else router.push(`/account?success=${encodeURIComponent('Account created successfully')}`) } catch (_) { - clearTimeout(timer) + setLoading(false) setError('There was an error with the credentials provided. Please try again.') } }, diff --git a/src/app/(pages)/not-found.tsx b/src/app/(pages)/not-found.tsx index 1a2f2a9..b3a6f3c 100644 --- a/src/app/(pages)/not-found.tsx +++ b/src/app/(pages)/not-found.tsx @@ -5,9 +5,13 @@ import { VerticalPadding } from '../_components/VerticalPadding' export default function NotFound() { return ( - -

    404

    -

    This page could not be found.

    + +

    404

    +

    The page you are looking for couldn't not be found.