@enomshop/paystack 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # Medusa v2 Paystack Payment Plugin
2
+
3
+ A robust, production-ready Paystack payment integration for **MedusaJS v2**. This plugin not only handles standard checkouts but also introduces advanced features like partial payments, an admin payment history widget, and a failsafe cron job for missed webhooks.
4
+
5
+ ## ✨ Features
6
+
7
+ - **Full Medusa v2 Compliance:** Uses the new isolated modules, file-based routing, and Admin SDK.
8
+ - **Standard Checkout:** Seamlessly process payments during the standard Medusa checkout flow.
9
+ - **Partial Payments / Installments:** Includes a custom storefront API route to allow customers to pay off an order in multiple installments. Works perfectly for both registered users and **Guest Customers** (automatically falls back to the original order email).
10
+ - **Admin Dashboard (Payments > Paystack):**
11
+ - **Live Account Balance:** Displays your live Paystack account balance and all-time total received directly from the Paystack API.
12
+ - **Revenue Graph:** A beautiful bar chart visualizing your captured revenue over time.
13
+ - **Endless Scroll History:** View your entire Paystack payment history directly in Medusa. Simply scroll to the bottom of the table to automatically load the next batch of payments.
14
+ - **Search:** Instantly search for a specific transaction using a Medusa Order ID (e.g., `1234`) or a Paystack Transaction Reference.
15
+ - **Failsafe Cron Job:** A scheduled job runs every 15 minutes to verify and capture pending payments in case Paystack webhooks are missed or delayed. Includes rate-limiting and race-condition prevention.
16
+ - **Currency Validation:** Fails fast if a customer attempts to checkout using a currency not supported by Paystack.
17
+ - **Secure Webhooks:** Verifies Paystack webhook signatures using HMAC SHA512.
18
+
19
+ ---
20
+
21
+ ## 🚀 1. Backend Installation & Integration
22
+
23
+ ### Step 1: Add the Plugin Files
24
+ Ensure all the provided files are placed in your Medusa v2 backend `src` directory:
25
+ - `src/services/paystack-payment-processor.ts`
26
+ - `src/lib/paystack.ts`
27
+ - `src/utils/currencyCode.ts`
28
+ - `src/api/store/orders/[id]/paystack-payment/route.ts`
29
+ - `src/admin/widgets/payment-history-widget.tsx`
30
+ - `src/jobs/sync-paystack-payments.ts`
31
+
32
+ ### Step 2: Environment Variables
33
+ Add your Paystack Secret Key to your backend `.env` file:
34
+ ```env
35
+ PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
36
+ ```
37
+
38
+ ### Step 3: Register the Payment Provider
39
+ Update your `medusa-config.ts` to register the Paystack payment provider in the Payment Module:
40
+
41
+ ```typescript
42
+ import { loadEnv, defineConfig } from "@medusajs/framework/utils"
43
+
44
+ loadEnv(process.env.NODE_ENV || "development", process.cwd())
45
+
46
+ module.exports = defineConfig({
47
+ projectConfig: {
48
+ databaseUrl: process.env.DATABASE_URL,
49
+ http: {
50
+ storeCors: process.env.STORE_CORS!,
51
+ adminCors: process.env.ADMIN_CORS!,
52
+ authCors: process.env.AUTH_CORS!,
53
+ jwtSecret: process.env.JWT_SECRET || "supersecret",
54
+ cookieSecret: process.env.COOKIE_SECRET || "supersecret",
55
+ },
56
+ },
57
+ modules: [
58
+ {
59
+ resolve: "@medusajs/payment",
60
+ options: {
61
+ providers: [
62
+ {
63
+ resolve: "./src/services/paystack-payment-processor",
64
+ id: "paystack",
65
+ options: {
66
+ secret_key: process.env.PAYSTACK_SECRET_KEY,
67
+ debug: process.env.NODE_ENV !== "production",
68
+ },
69
+ },
70
+ ],
71
+ },
72
+ },
73
+ ],
74
+ })
75
+ ```
76
+
77
+ ### Step 4: Configure Webhooks in Paystack
78
+ Log in to your Paystack Dashboard, go to **Settings > API Keys & Webhooks**, and set your webhook URL to:
79
+ ```text
80
+ https://<YOUR_MEDUSA_BACKEND_URL>/hooks/payment/paystack
81
+ ```
82
+ *Note: Medusa v2 automatically routes webhooks to the provider based on the ID (`paystack`).*
83
+
84
+ ---
85
+
86
+ ## 💻 2. Storefront Implementation (Next.js / Fresh.js)
87
+
88
+ You can use this plugin in two ways on your storefront: for standard checkout, and for partial payments on an existing order.
89
+
90
+ ### Scenario A: Standard Checkout Flow
91
+ During a standard checkout, you initialize the payment session using the Medusa JS Client.
92
+
93
+ ```javascript
94
+ // Example in Next.js or Fresh.js
95
+ import { medusaClient } from "@lib/config"; // Your Medusa JS Client instance
96
+
97
+ const handleStandardCheckout = async (cartId, email) => {
98
+ // 1. Initialize payment sessions for the cart
99
+ await medusaClient.carts.createPaymentSessions(cartId);
100
+
101
+ // 2. Select Paystack as the payment session
102
+ const { cart } = await medusaClient.carts.setPaymentSession(cartId, {
103
+ provider_id: "paystack",
104
+ });
105
+
106
+ // 3. Get the Paystack authorization URL from the session data
107
+ const paystackSession = cart.payment_collection.payment_sessions.find(
108
+ (s) => s.provider_id === "paystack"
109
+ );
110
+
111
+ const authUrl = paystackSession.data.paystackTxAuthorizationUrl;
112
+
113
+ // 4. Redirect the user to Paystack to complete payment
114
+ window.location.href = authUrl;
115
+ };
116
+ ```
117
+
118
+ ### Scenario B: Partial Payments / Installments
119
+ If an order already exists and the customer wants to pay a portion of the remaining balance, use the custom API route we created.
120
+
121
+ ```javascript
122
+ // Example in Next.js or Fresh.js (e.g., on an Order Details page)
123
+ import { useState } from "react";
124
+
125
+ export default function PartialPaymentButton({ orderId, remainingBalance, customerEmail }) {
126
+ const [amountToPay, setAmountToPay] = useState(0);
127
+ const [loading, setLoading] = useState(false);
128
+
129
+ const handlePartialPayment = async () => {
130
+ if (amountToPay <= 0 || amountToPay > remainingBalance) {
131
+ alert("Invalid amount");
132
+ return;
133
+ }
134
+
135
+ setLoading(true);
136
+ try {
137
+ // Call the custom Medusa backend API route
138
+ const response = await fetch(`${process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL}/store/orders/${orderId}/paystack-payment`, {
139
+ method: "POST",
140
+ headers: {
141
+ "Content-Type": "application/json",
142
+ },
143
+ body: JSON.stringify({
144
+ amount: amountToPay,
145
+ email: customerEmail,
146
+ callback_url: `${window.location.origin}/order/${orderId}/success`, // Redirect back here after payment
147
+ metadata: {
148
+ note: "Partial installment payment",
149
+ }
150
+ }),
151
+ });
152
+
153
+ if (!response.ok) {
154
+ const error = await response.json();
155
+ throw new Error(error.message);
156
+ }
157
+
158
+ const { payment_session } = await response.json();
159
+
160
+ // Redirect the user to Paystack
161
+ window.location.href = payment_session.data.paystackTxAuthorizationUrl;
162
+ } catch (err) {
163
+ console.error(err);
164
+ alert("Failed to initiate payment: " + err.message);
165
+ } finally {
166
+ setLoading(false);
167
+ }
168
+ };
169
+
170
+ return (
171
+ <div>
172
+ <h3>Remaining Balance: {remainingBalance}</h3>
173
+ <input
174
+ type="number"
175
+ value={amountToPay}
176
+ onChange={(e) => setAmountToPay(Number(e.target.value))}
177
+ max={remainingBalance}
178
+ />
179
+ <button onClick={handlePartialPayment} disabled={loading}>
180
+ {loading ? "Processing..." : "Pay Installment"}
181
+ </button>
182
+ </div>
183
+ );
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 🛡️ 3. Production Readiness Features Explained
190
+
191
+ 1. **Amount Handling:** Medusa stores amounts in the lowest denomination (e.g., cents/kobo). The plugin safely passes this exact value to Paystack (`Math.round(Number(amount))`) without dangerous multipliers, preventing accidental overcharging.
192
+ 2. **Cron Job Failsafe (`src/jobs/sync-paystack-payments.ts`):**
193
+ - Runs every 15 minutes.
194
+ - Only checks payments older than 15 minutes to prevent race conditions with incoming webhooks.
195
+ - Includes a `200ms` sleep delay between API calls to prevent hitting Paystack's rate limits (`429 Too Many Requests`) if you have a large backlog of abandoned checkouts.
196
+ 3. **Currency Validation:** The processor checks if the cart's currency is supported by Paystack (`NGN`, `GHS`, `ZAR`, `USD`, `KES`, `EGP`, `RWF`) *before* making an API call, failing fast and returning a clean error to the storefront.
197
+ 4. **Overpayment Prevention:** The partial payment API route calculates the total captured amount of all previous payments. If a customer tries to pay more than the remaining balance, the API rejects the request.
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@enomshop/paystack",
3
+ "version": "0.0.1",
4
+ "description": "Medusa v2 plugin for Paystack payment provider",
5
+ "author": "Enomshop",
6
+ "license": "MIT",
7
+ "files": [
8
+ ".medusa/server",
9
+ "!.medusa/server/**/*.ts",
10
+ "!.medusa/server/**/*.tsbuildinfo"
11
+ ],
12
+ "exports": {
13
+ "./package.json": "./package.json",
14
+ "./workflows": "./.medusa/server/src/workflows/index.js",
15
+ "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
16
+ "./modules/*": "./.medusa/server/src/modules/*/index.js",
17
+ "./providers/*": "./.medusa/server/src/providers/*/index.js",
18
+ "./*": "./.medusa/server/src/*.js",
19
+ "./admin": {
20
+ "import": "./.medusa/server/src/admin/index.mjs",
21
+ "require": "./.medusa/server/src/admin/index.js",
22
+ "default": "./.medusa/server/src/admin/index.js"
23
+ }
24
+ },
25
+ "keywords": [
26
+ "medusa",
27
+ "plugin",
28
+ "medusa-plugin-other",
29
+ "medusa-plugin",
30
+ "medusa-v2"
31
+ ],
32
+ "scripts": {
33
+ "build": "medusa plugin:build && find .medusa/server -name '*.ts' -delete",
34
+ "dev": "medusa plugin:develop",
35
+ "prepublishOnly": "npm run build"
36
+ },
37
+ "dependencies": {
38
+ "recharts": "^3.8.0"
39
+ },
40
+ "devDependencies": {
41
+ "@medusajs/admin-sdk": "2.12.4",
42
+ "@medusajs/cli": "2.12.4",
43
+ "@medusajs/framework": "2.12.4",
44
+ "@medusajs/icons": "2.12.4",
45
+ "@medusajs/medusa": "2.12.4",
46
+ "@medusajs/test-utils": "2.12.4",
47
+ "@medusajs/ui": "4.0.25",
48
+ "@swc/core": "^1.7.28",
49
+ "@types/node": "^20.0.0",
50
+ "@types/react": "^18.3.2",
51
+ "@types/react-dom": "^18.2.25",
52
+ "prop-types": "^15.8.1",
53
+ "react": "^18.2.0",
54
+ "react-dom": "^18.2.0",
55
+ "ts-node": "^10.9.2",
56
+ "typescript": "^5.6.2",
57
+ "vite": "^5.2.11",
58
+ "yalc": "^1.0.0-pre.53"
59
+ },
60
+ "peerDependencies": {
61
+ "@medusajs/admin-sdk": "2.12.4",
62
+ "@medusajs/cli": "2.12.4",
63
+ "@medusajs/framework": "2.12.4",
64
+ "@medusajs/icons": "2.12.4",
65
+ "@medusajs/medusa": "2.12.4",
66
+ "@medusajs/test-utils": "2.12.4",
67
+ "@medusajs/ui": "4.0.25"
68
+ },
69
+ "engines": {
70
+ "node": ">=20"
71
+ },
72
+ "packageManager": "yarn@4.12.0"
73
+ }