medusa-paystack 0.1.0

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.
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ const widgetModule = { widgets: [] };
3
+ const routeModule = {
4
+ routes: []
5
+ };
6
+ const menuItemModule = {
7
+ menuItems: []
8
+ };
9
+ const formModule = { customFields: {} };
10
+ const displayModule = {
11
+ displays: {}
12
+ };
13
+ const i18nModule = { resources: {} };
14
+ const plugin = {
15
+ widgetModule,
16
+ routeModule,
17
+ menuItemModule,
18
+ formModule,
19
+ displayModule,
20
+ i18nModule
21
+ };
22
+ module.exports = plugin;
@@ -0,0 +1,23 @@
1
+ const widgetModule = { widgets: [] };
2
+ const routeModule = {
3
+ routes: []
4
+ };
5
+ const menuItemModule = {
6
+ menuItems: []
7
+ };
8
+ const formModule = { customFields: {} };
9
+ const displayModule = {
10
+ displays: {}
11
+ };
12
+ const i18nModule = { resources: {} };
13
+ const plugin = {
14
+ widgetModule,
15
+ routeModule,
16
+ menuItemModule,
17
+ formModule,
18
+ displayModule,
19
+ i18nModule
20
+ };
21
+ export {
22
+ plugin as default
23
+ };
@@ -0,0 +1,6 @@
1
+ import { ModuleProvider, Modules } from "@medusajs/framework/utils";
2
+ import PaystackProviderService from "./service";
3
+ export default ModuleProvider(Modules.PAYMENT, {
4
+ services: [PaystackProviderService],
5
+ });
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvcHJvdmlkZXJzL3BheXN0YWNrL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxjQUFjLEVBQUUsT0FBTyxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDcEUsT0FBTyx1QkFBdUIsTUFBTSxXQUFXLENBQUM7QUFFaEQsZUFBZSxjQUFjLENBQUMsT0FBTyxDQUFDLE9BQU8sRUFBRTtJQUM3QyxRQUFRLEVBQUUsQ0FBQyx1QkFBdUIsQ0FBQztDQUNwQyxDQUFDLENBQUMifQ==
@@ -0,0 +1,51 @@
1
+ import { createHmac, timingSafeEqual } from "crypto";
2
+ export function verifyPaystackSignature(rawBody, signature, secret) {
3
+ const expected = createHmac("sha512", secret).update(rawBody, "utf8").digest("hex");
4
+ if (expected.length !== signature.length)
5
+ return false;
6
+ try {
7
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ export function webhookAction(eventName, eventData, signatureValid) {
14
+ if (!signatureValid)
15
+ return { action: "not_supported" };
16
+ if (eventName === "charge.success") {
17
+ const sid = eventData.metadata?.session_id;
18
+ if (!sid)
19
+ return { action: "not_supported" };
20
+ return {
21
+ action: "captured",
22
+ data: { session_id: sid, amount: (eventData.amount ?? 0) / 100 },
23
+ };
24
+ }
25
+ return { action: "not_supported" };
26
+ }
27
+ export function assertChargeMatches(args) {
28
+ if (args.status !== "success")
29
+ return { ok: false, reason: `status ${args.status}` };
30
+ if (args.currency.toUpperCase() !== args.expectedCurrency.toUpperCase())
31
+ return { ok: false, reason: `currency ${args.currency}` };
32
+ if (args.amount !== args.expectedAmount)
33
+ return { ok: false, reason: `amount ${args.amount} != ${args.expectedAmount}` };
34
+ return { ok: true };
35
+ }
36
+ export function makeReference(seed, prefix = "") {
37
+ return `${prefix}${seed}`;
38
+ }
39
+ export function buildInitializePayload(args) {
40
+ return {
41
+ email: args.email,
42
+ amount: Math.round(args.amount * 100),
43
+ currency: args.currencyCode.toUpperCase(),
44
+ reference: args.reference,
45
+ metadata: { session_id: args.sessionId },
46
+ };
47
+ }
48
+ export function buildRefundPayload(reference, amount) {
49
+ return { transaction: reference, amount: Math.round(amount * 100) };
50
+ }
51
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGliLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vc3JjL3Byb3ZpZGVycy9wYXlzdGFjay9saWIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxlQUFlLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFFckQsTUFBTSxVQUFVLHVCQUF1QixDQUNyQyxPQUFlLEVBQ2YsU0FBaUIsRUFDakIsTUFBYztJQUVkLE1BQU0sUUFBUSxHQUFHLFVBQVUsQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxNQUFNLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDcEYsSUFBSSxRQUFRLENBQUMsTUFBTSxLQUFLLFNBQVMsQ0FBQyxNQUFNO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDdkQsSUFBSSxDQUFDO1FBQ0gsT0FBTyxlQUFlLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUM7SUFDeEUsQ0FBQztJQUFDLE1BQU0sQ0FBQztRQUNQLE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztBQUNILENBQUM7QUFZRCxNQUFNLFVBQVUsYUFBYSxDQUMzQixTQUE2QixFQUM3QixTQUEyQixFQUMzQixjQUF1QjtJQUV2QixJQUFJLENBQUMsY0FBYztRQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsZUFBZSxFQUFFLENBQUM7SUFFeEQsSUFBSSxTQUFTLEtBQUssZ0JBQWdCLEVBQUUsQ0FBQztRQUNuQyxNQUFNLEdBQUcsR0FBRyxTQUFTLENBQUMsUUFBUSxFQUFFLFVBQVUsQ0FBQztRQUMzQyxJQUFJLENBQUMsR0FBRztZQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsZUFBZSxFQUFFLENBQUM7UUFDN0MsT0FBTztZQUNMLE1BQU0sRUFBRSxVQUFVO1lBQ2xCLElBQUksRUFBRSxFQUFFLFVBQVUsRUFBRSxHQUFHLEVBQUUsTUFBTSxFQUFFLENBQUMsU0FBUyxDQUFDLE1BQU0sSUFBSSxDQUFDLENBQUMsR0FBRyxHQUFHLEVBQUU7U0FDakUsQ0FBQztJQUNKLENBQUM7SUFFRCxPQUFPLEVBQUUsTUFBTSxFQUFFLGVBQWUsRUFBRSxDQUFDO0FBQ3JDLENBQUM7QUFFRCxNQUFNLFVBQVUsbUJBQW1CLENBQUMsSUFNbkM7SUFDQyxJQUFJLElBQUksQ0FBQyxNQUFNLEtBQUssU0FBUztRQUFFLE9BQU8sRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxVQUFVLElBQUksQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDO0lBQ3JGLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxXQUFXLEVBQUUsS0FBSyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsV0FBVyxFQUFFO1FBQ3JFLE9BQU8sRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLE1BQU0sRUFBRSxZQUFZLElBQUksQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDO0lBQzVELElBQUksSUFBSSxDQUFDLE1BQU0sS0FBSyxJQUFJLENBQUMsY0FBYztRQUNyQyxPQUFPLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsVUFBVSxJQUFJLENBQUMsTUFBTSxPQUFPLElBQUksQ0FBQyxjQUFjLEVBQUUsRUFBRSxDQUFDO0lBQ2xGLE9BQU8sRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLENBQUM7QUFDdEIsQ0FBQztBQUVELE1BQU0sVUFBVSxhQUFhLENBQUMsSUFBWSxFQUFFLE1BQU0sR0FBRyxFQUFFO0lBQ3JELE9BQU8sR0FBRyxNQUFNLEdBQUcsSUFBSSxFQUFFLENBQUM7QUFDNUIsQ0FBQztBQUVELE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxJQU10QztJQU9DLE9BQU87UUFDTCxLQUFLLEVBQUUsSUFBSSxDQUFDLEtBQUs7UUFDakIsTUFBTSxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sR0FBRyxHQUFHLENBQUM7UUFDckMsUUFBUSxFQUFFLElBQUksQ0FBQyxZQUFZLENBQUMsV0FBVyxFQUFFO1FBQ3pDLFNBQVMsRUFBRSxJQUFJLENBQUMsU0FBUztRQUN6QixRQUFRLEVBQUUsRUFBRSxVQUFVLEVBQUUsSUFBSSxDQUFDLFNBQVMsRUFBRTtLQUN6QyxDQUFDO0FBQ0osQ0FBQztBQUVELE1BQU0sVUFBVSxrQkFBa0IsQ0FDaEMsU0FBaUIsRUFDakIsTUFBYztJQUVkLE9BQU8sRUFBRSxXQUFXLEVBQUUsU0FBUyxFQUFFLE1BQU0sRUFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sR0FBRyxHQUFHLENBQUMsRUFBRSxDQUFDO0FBQ3RFLENBQUMifQ==
@@ -0,0 +1,160 @@
1
+ import { AbstractPaymentProvider } from "@medusajs/framework/utils";
2
+ import { assertChargeMatches, buildInitializePayload, buildRefundPayload, makeReference, verifyPaystackSignature, webhookAction, } from "./lib";
3
+ const PAYSTACK_API = "https://api.paystack.co";
4
+ class PaystackProviderService extends AbstractPaymentProvider {
5
+ constructor(cradle, options) {
6
+ super(cradle, options);
7
+ this.options_ = options ?? {};
8
+ }
9
+ get secretKey() {
10
+ return this.options_?.secret_key ?? process.env.PAYSTACK_SECRET_KEY ?? "";
11
+ }
12
+ authHeader() {
13
+ return {
14
+ Authorization: `Bearer ${this.secretKey}`,
15
+ "Content-Type": "application/json",
16
+ };
17
+ }
18
+ async initiatePayment(input) {
19
+ const { amount, currency_code, context } = input;
20
+ const data = input.data;
21
+ const email = context?.customer?.email ??
22
+ (typeof data?.email === "string" ? data.email : undefined);
23
+ if (!email) {
24
+ throw new Error("Paystack: a customer email is required to initiate payment");
25
+ }
26
+ const seed = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
27
+ const reference = makeReference(seed, this.options_.reference_prefix);
28
+ const payload = buildInitializePayload({
29
+ amount: Number(amount),
30
+ currencyCode: currency_code,
31
+ email,
32
+ reference,
33
+ sessionId: data?.session_id,
34
+ });
35
+ const res = await fetch(`${PAYSTACK_API}/transaction/initialize`, {
36
+ method: "POST",
37
+ headers: this.authHeader(),
38
+ body: JSON.stringify(payload),
39
+ });
40
+ const json = await res.json();
41
+ if (!json.status || !json.data) {
42
+ throw new Error(`Paystack initiate failed: ${json.message}`);
43
+ }
44
+ const { access_code, authorization_url } = json.data;
45
+ return {
46
+ id: reference,
47
+ data: {
48
+ reference,
49
+ access_code,
50
+ authorization_url,
51
+ amount: Number(amount),
52
+ currency: currency_code.toUpperCase(),
53
+ },
54
+ };
55
+ }
56
+ async authorizePayment(input) {
57
+ const data = input.data ?? {};
58
+ const reference = data.reference;
59
+ if (!reference)
60
+ return { status: "error", data };
61
+ const res = await fetch(`${PAYSTACK_API}/transaction/verify/${encodeURIComponent(reference)}`, { headers: this.authHeader() });
62
+ const json = await res.json();
63
+ if (!json.status || !json.data)
64
+ return { status: "error", data };
65
+ const { status, currency, amount } = json.data;
66
+ const expectedAmount = Math.round(Number(data.amount ?? 0) * 100);
67
+ const expectedCurrency = String(data.currency ?? "").toUpperCase();
68
+ const check = assertChargeMatches({
69
+ status,
70
+ currency,
71
+ amount,
72
+ expectedAmount,
73
+ expectedCurrency,
74
+ });
75
+ if (!check.ok) {
76
+ return { status: "error", data: { ...data, ...json.data, reason: check.reason } };
77
+ }
78
+ return { status: "captured", data: { ...data, ...json.data } };
79
+ }
80
+ async capturePayment(input) {
81
+ // Paystack auto-captures on transaction success; this is a no-op.
82
+ return { data: input.data };
83
+ }
84
+ async getPaymentStatus(input) {
85
+ const data = input.data ?? {};
86
+ const reference = data.reference;
87
+ if (!reference)
88
+ return { status: "pending" };
89
+ const res = await fetch(`${PAYSTACK_API}/transaction/verify/${encodeURIComponent(reference)}`, { headers: this.authHeader() });
90
+ const json = await res.json();
91
+ if (!json.status || !json.data)
92
+ return { status: "error" };
93
+ switch (json.data.status) {
94
+ case "success":
95
+ return { status: "captured" };
96
+ case "failed":
97
+ case "reversed":
98
+ return { status: "error" };
99
+ case "abandoned":
100
+ return { status: "canceled" };
101
+ default:
102
+ return { status: "pending" };
103
+ }
104
+ }
105
+ async refundPayment(input) {
106
+ const data = input.data ?? {};
107
+ const reference = data.reference;
108
+ const res = await fetch(`${PAYSTACK_API}/refund`, {
109
+ method: "POST",
110
+ headers: this.authHeader(),
111
+ body: JSON.stringify(buildRefundPayload(reference, Number(input.amount))),
112
+ });
113
+ const json = (await res.json());
114
+ if (!res.ok || !json.status) {
115
+ throw new Error(`Paystack refund failed: ${json.message ?? `HTTP ${res.status}`}`);
116
+ }
117
+ return { data };
118
+ }
119
+ async cancelPayment(input) {
120
+ // Paystack has no explicit cancel API for pending transactions.
121
+ return { data: input.data };
122
+ }
123
+ async deletePayment(input) {
124
+ return { data: input.data };
125
+ }
126
+ async retrievePayment(input) {
127
+ const data = input.data ?? {};
128
+ const reference = data.reference;
129
+ if (!reference)
130
+ return { data };
131
+ const res = await fetch(`${PAYSTACK_API}/transaction/verify/${encodeURIComponent(reference)}`, { headers: this.authHeader() });
132
+ const json = await res.json();
133
+ if (!json.status || !json.data)
134
+ return { data };
135
+ return { data: { ...data, ...json.data } };
136
+ }
137
+ async updatePayment(input) {
138
+ // Re-initiate to refresh the session when amount/currency changes;
139
+ // forward input.data so session_id is preserved in Paystack metadata.
140
+ const result = await this.initiatePayment({
141
+ amount: input.amount,
142
+ currency_code: input.currency_code,
143
+ context: input.context,
144
+ data: input.data,
145
+ });
146
+ return { data: result.data };
147
+ }
148
+ async getWebhookActionAndData(payload) {
149
+ const { data, rawData, headers } = payload;
150
+ const signature = headers["x-paystack-signature"] ?? "";
151
+ const raw = Buffer.isBuffer(rawData) ? rawData.toString("utf8") : String(rawData);
152
+ const signatureValid = verifyPaystackSignature(raw, signature, this.secretKey);
153
+ const event = data;
154
+ const eventData = (event.data ?? {});
155
+ return webhookAction(event.event, eventData, signatureValid);
156
+ }
157
+ }
158
+ PaystackProviderService.identifier = "paystack";
159
+ export default PaystackProviderService;
160
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9wcm92aWRlcnMvcGF5c3RhY2svc2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFzQkEsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDcEUsT0FBTyxFQUNMLG1CQUFtQixFQUNuQixzQkFBc0IsRUFDdEIsa0JBQWtCLEVBQ2xCLGFBQWEsRUFHYix1QkFBdUIsRUFDdkIsYUFBYSxHQUNkLE1BQU0sT0FBTyxDQUFDO0FBT2YsTUFBTSxZQUFZLEdBQUcseUJBQXlCLENBQUM7QUFFL0MsTUFBTSx1QkFBd0IsU0FBUSx1QkFBd0M7SUFLNUUsWUFBWSxNQUErQixFQUFFLE9BQXdCO1FBQ25FLEtBQUssQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7UUFDdkIsSUFBSSxDQUFDLFFBQVEsR0FBRyxPQUFPLElBQUksRUFBRSxDQUFDO0lBQ2hDLENBQUM7SUFFRCxJQUFZLFNBQVM7UUFDbkIsT0FBTyxJQUFJLENBQUMsUUFBUSxFQUFFLFVBQVUsSUFBSSxPQUFPLENBQUMsR0FBRyxDQUFDLG1CQUFtQixJQUFJLEVBQUUsQ0FBQztJQUM1RSxDQUFDO0lBRU8sVUFBVTtRQUNoQixPQUFPO1lBQ0wsYUFBYSxFQUFFLFVBQVUsSUFBSSxDQUFDLFNBQVMsRUFBRTtZQUN6QyxjQUFjLEVBQUUsa0JBQWtCO1NBQ25DLENBQUM7SUFDSixDQUFDO0lBRUQsS0FBSyxDQUFDLGVBQWUsQ0FBQyxLQUEyQjtRQUMvQyxNQUFNLEVBQUUsTUFBTSxFQUFFLGFBQWEsRUFBRSxPQUFPLEVBQUUsR0FBRyxLQUFLLENBQUM7UUFDakQsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLElBQTJDLENBQUM7UUFFL0QsTUFBTSxLQUFLLEdBQ1QsT0FBTyxFQUFFLFFBQVEsRUFBRSxLQUFLO1lBQ3hCLENBQUMsT0FBTyxJQUFJLEVBQUUsS0FBSyxLQUFL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
package/AGENTS.md ADDED
@@ -0,0 +1,147 @@
1
+ # AGENTS.md — integrating `medusa-paystack`
2
+
3
+ Audience: an AI coding agent (Claude Code, Cursor, etc.) adding Paystack payments to
4
+ a user's **Medusa v2** store. Treat this as a checklist. Exact values matter — copy
5
+ them verbatim.
6
+
7
+ ## What this package is
8
+
9
+ - A **Paystack payment provider for Medusa v2** (targets `@medusajs/medusa` **2.16.x**).
10
+ - It is a **payment module provider**, resolved at `medusa-paystack/providers/paystack`.
11
+ Provider `identifier` = `paystack`.
12
+ - It charges in the **cart's own `currency_code`** — no FX/conversion. The merchant's
13
+ Paystack account must be enabled for those currencies (Paystack supports GHS, NGN,
14
+ ZAR, USD, KES).
15
+
16
+ ## The one thing agents get wrong
17
+
18
+ This plugin is the **server side only**. It renders **no checkout UI** — no Medusa
19
+ payment provider does (the official Stripe plugin is the same). If you only register
20
+ the provider, the customer **cannot pay**. You MUST also implement the storefront
21
+ step (Section 4), or checkout is broken. Do not report the task complete after
22
+ Section 2.
23
+
24
+ ## Gather first
25
+
26
+ - `PAYSTACK_SECRET_KEY` (backend) and the Paystack **public** key (storefront).
27
+ Test keys look like `sk_test_…` / `pk_test_…`.
28
+ - Confirm the backend is on Medusa **2.16.x**: `npm ls @medusajs/medusa`.
29
+ - Identify the storefront app (Next.js Medusa starter, custom, etc.) and whether it
30
+ uses `@medusajs/js-sdk`.
31
+
32
+ ## 1. Install (in the Medusa backend)
33
+
34
+ ```bash
35
+ npm install medusa-paystack
36
+ ```
37
+
38
+ ## 2. Register the provider — `medusa-config.ts`
39
+
40
+ Add it to the **payment module's `providers` array**. Do NOT put it in the top-level
41
+ `plugins` array, and do NOT register it as a standalone module.
42
+
43
+ ```ts
44
+ {
45
+ resolve: "@medusajs/medusa/payment",
46
+ options: {
47
+ providers: [
48
+ {
49
+ resolve: "medusa-paystack/providers/paystack",
50
+ id: "paystack",
51
+ options: {
52
+ secret_key: process.env.PAYSTACK_SECRET_KEY,
53
+ // reference_prefix: "MY-", // optional, default ""
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ }
59
+ ```
60
+
61
+ If a `@medusajs/medusa/payment` block already exists, **append** to its `providers`
62
+ array — do not create a second payment module.
63
+
64
+ The provider's runtime id becomes `pp_<identifier>_<id>` = **`pp_paystack_paystack`**.
65
+ The storefront and webhook depend on this exact string. If you choose a different
66
+ `id`, update Sections 4 and 5 to match (`pp_paystack_<id>` and
67
+ `/hooks/payment/paystack_<id>`).
68
+
69
+ ## 3. Environment (backend `.env`)
70
+
71
+ ```
72
+ PAYSTACK_SECRET_KEY=sk_test_xxx
73
+ ```
74
+
75
+ ## 4. Storefront — REQUIRED, do not skip
76
+
77
+ Without this, the provider is registered but no payment can be taken.
78
+
79
+ - Install the popup lib in the **storefront** app: `npm install @paystack/inline-js`.
80
+ - Expose the **public** key to the browser, e.g.
81
+ `NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY=pk_test_xxx`.
82
+ - Implement a pay button with this flow (using `@medusajs/js-sdk`):
83
+ 1. `sdk.store.payment.initiatePaymentSession(cart, { provider_id: "pp_paystack_paystack", data: { email } })`
84
+ — **email is required**; Paystack rejects a session without one.
85
+ 2. Read `access_code` from the session whose `provider_id === "pp_paystack_paystack"`.
86
+ 3. `new PaystackPop().resumeTransaction(access_code, { onSuccess, onCancel, onError })`
87
+ (from `@paystack/inline-js`). Alternatively redirect the browser to the
88
+ session's `authorization_url`.
89
+ 4. On success: `await sdk.store.cart.complete(cartId)` → creates the order.
90
+
91
+ Copy the full, working component from this package's **README → "Storefront
92
+ integration"** and adapt the env-var names to the project. The README also documents
93
+ the redirect-based alternative.
94
+
95
+ ## 5. Webhook
96
+
97
+ In the Paystack dashboard, set the webhook URL to:
98
+
99
+ ```
100
+ https://<backend-host>/hooks/payment/paystack_paystack
101
+ ```
102
+
103
+ The path segment is `<identifier>_<id>` = `paystack_paystack`. The provider verifies
104
+ `x-paystack-signature` (HMAC-SHA512 over the raw body with the secret key) and
105
+ captures the session on `charge.success`.
106
+
107
+ ## 6. Enable for regions
108
+
109
+ Add **Paystack** as a payment provider to each region that should accept it: Admin →
110
+ **Settings → Regions**, or via the Admin API.
111
+
112
+ ## 7. Verify before reporting done
113
+
114
+ - Backend boots with no errors (`npx medusa develop` or the project's dev command).
115
+ - Admin shows Paystack as an available provider on a region.
116
+ - A test checkout with Paystack **test** keys: pay button → Paystack popup → success
117
+ → order created in Admin.
118
+ - Webhook: replay a `charge.success` from the Paystack dashboard and confirm the
119
+ payment session captures.
120
+
121
+ ## Do / Don't
122
+
123
+ - DO pass the customer email into `initiatePaymentSession` `data`.
124
+ - DO use the exact `provider_id` `pp_paystack_paystack` in the storefront and the
125
+ matching `/hooks/payment/paystack_paystack` webhook path.
126
+ - DON'T add `medusa-paystack` to the top-level `plugins` array — it's a payment
127
+ provider, registered under the payment module's `providers`.
128
+ - DON'T pre-convert amounts to subunits — Medusa passes **major-unit** amounts and
129
+ the provider multiplies by 100 internally.
130
+ - DON'T hardcode a currency — the provider uses the cart's `currency_code`.
131
+ - DON'T treat the client `onSuccess` as proof of payment — the server re-verifies
132
+ status, amount, and currency in `authorizePayment` during `cart.complete`.
133
+
134
+ ## Options reference
135
+
136
+ | Option | Required | Default | Notes |
137
+ | --- | --- | --- | --- |
138
+ | `secret_key` | yes | `process.env.PAYSTACK_SECRET_KEY` | server API calls + webhook signature verification |
139
+ | `reference_prefix` | no | `""` | prepended to generated transaction references |
140
+
141
+ ## Provider methods (reference)
142
+
143
+ `initiatePayment` (creates the Paystack transaction), `authorizePayment` (verifies
144
+ status/amount/currency), `capturePayment` (no-op — Paystack auto-captures on
145
+ success), `getPaymentStatus`, `refundPayment`, `cancelPayment`/`deletePayment`
146
+ (no-ops), `updatePayment` (re-initiates the session), `getWebhookActionAndData`
147
+ (signature check + `charge.success` → capture).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nuette
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # medusa-paystack
2
+
3
+ A [Paystack](https://paystack.com) payment provider for **Medusa v2**.
4
+
5
+ Unlike store-specific forks, this provider charges in the **cart's own currency**
6
+ (`currency_code`). Paystack natively supports GHS, NGN, ZAR, USD, and KES — make
7
+ sure your Paystack account is enabled for the currencies your store sells in. This
8
+ plugin does **not** perform any currency conversion.
9
+
10
+ ## Requirements
11
+
12
+ - Medusa **2.16.x**
13
+ - A Paystack secret key
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install medusa-paystack
19
+ ```
20
+
21
+ ## Configure
22
+
23
+ Add the provider to the Payment Module in your `medusa-config.ts`:
24
+
25
+ ```ts
26
+ module.exports = defineConfig({
27
+ modules: [
28
+ {
29
+ resolve: "@medusajs/medusa/payment",
30
+ options: {
31
+ providers: [
32
+ {
33
+ resolve: "medusa-paystack/providers/paystack",
34
+ id: "paystack",
35
+ options: {
36
+ secret_key: process.env.PAYSTACK_SECRET_KEY,
37
+ // reference_prefix: "MY-", // optional, default ""
38
+ },
39
+ },
40
+ ],
41
+ },
42
+ },
43
+ ],
44
+ })
45
+ ```
46
+
47
+ ### Options
48
+
49
+ | Option | Required | Default | Description |
50
+ | --- | --- | --- | --- |
51
+ | `secret_key` | yes | `process.env.PAYSTACK_SECRET_KEY` | Paystack secret key, used for API calls and webhook signature verification. |
52
+ | `reference_prefix` | no | `""` | Prefix prepended to generated transaction references. |
53
+
54
+ ## Enable in the admin
55
+
56
+ In **Settings → Regions**, add **Paystack** as a payment provider for each region
57
+ that should accept it.
58
+
59
+ ## Storefront integration
60
+
61
+ This plugin is the **server-side** half of the payment flow. Like every Medusa
62
+ payment provider (including the official Stripe plugin), it renders **no checkout
63
+ UI** — your storefront drives the payment, then completes the cart. The plugin's
64
+ job is to start the Paystack transaction and then securely verify the status,
65
+ amount, currency, and webhook signature on the server. A storefront cannot accept
66
+ payment with the plugin alone; you must add the steps below.
67
+
68
+ The flow your storefront implements:
69
+
70
+ 1. **Initialize a payment session** on the cart, selecting this provider. The
71
+ provider returns Paystack's `access_code` (and `authorization_url`) in the
72
+ session's `data`.
73
+ 2. **Collect payment** with Paystack — open Paystack Inline with the `access_code`,
74
+ or redirect the browser to `authorization_url`.
75
+ 3. **Complete the cart** on success — this triggers the provider's server-side
76
+ verification and creates the order.
77
+
78
+ ### Prerequisites
79
+
80
+ - Your **Paystack public key** exposed to the browser (e.g.
81
+ `NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY`). The plugin uses the *secret* key on the
82
+ server; the storefront only needs the *public* key.
83
+ - The Medusa JS SDK (`@medusajs/js-sdk`) with your publishable API key.
84
+ - For the inline popup: `npm install @paystack/inline-js`.
85
+
86
+ > **`provider_id`** is `pp_<identifier>_<id>` — `pp_paystack_` plus the `id` you set
87
+ > in `medusa-config.ts`. With `id: "paystack"` it is `pp_paystack_paystack`.
88
+
89
+ ### Example (React / Next.js)
90
+
91
+ ```tsx
92
+ "use client";
93
+
94
+ import { useState } from "react";
95
+ import Medusa from "@medusajs/js-sdk";
96
+
97
+ const sdk = new Medusa({
98
+ baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
99
+ publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,
100
+ });
101
+
102
+ const PROVIDER_ID = "pp_paystack_paystack"; // must match pp_<identifier>_<id>
103
+
104
+ export function PayWithPaystack({ cartId, email }: { cartId: string; email: string }) {
105
+ const [error, setError] = useState<string>();
106
+ const [loading, setLoading] = useState(false);
107
+
108
+ async function pay() {
109
+ setLoading(true);
110
+ setError(undefined);
111
+ try {
112
+ // 1. Initialize the Paystack payment session on the cart.
113
+ // Paystack requires a customer email — pass it in `data`.
114
+ const { cart } = await sdk.store.cart.retrieve(cartId);
115
+ const { payment_collection } = await sdk.store.payment.initiatePaymentSession(
116
+ cart,
117
+ { provider_id: PROVIDER_ID, data: { email } }
118
+ );
119
+ const session = payment_collection.payment_sessions?.find(
120
+ (s) => s.provider_id === PROVIDER_ID
121
+ );
122
+ const accessCode = session?.data?.access_code as string | undefined;
123
+ if (!accessCode) throw new Error("No access_code returned from the payment session");
124
+
125
+ // 2. Open Paystack Inline with the access code.
126
+ const mod = await import("@paystack/inline-js");
127
+ const PaystackPop = (mod.default ?? mod) as new () => {
128
+ resumeTransaction(
129
+ accessCode: string,
130
+ cb: {
131
+ onSuccess?: () => void;
132
+ onCancel?: () => void;
133
+ onError?: (e: { message: string }) => void;
134
+ }
135
+ ): void;
136
+ };
137
+
138
+ new PaystackPop().resumeTransaction(accessCode, {
139
+ onSuccess: async () => {
140
+ // 3. Complete the cart -> runs the provider's server-side verification
141
+ // (status + amount + currency) and creates the order.
142
+ const res = await sdk.store.cart.complete(cartId);
143
+ if (res.type === "order") {
144
+ window.location.href = `/order/confirmed/${res.order.id}`;
145
+ } else {
146
+ setError(res.error?.message ?? "Order could not be completed.");
147
+ }
148
+ },
149
+ onCancel: () => setError("Payment cancelled — your cart is still saved."),
150
+ onError: (e) => setError(e.message),
151
+ });
152
+ } catch (e) {
153
+ setError(e instanceof Error ? e.message : "Could not start payment.");
154
+ setLoading(false);
155
+ }
156
+ }
157
+
158
+ return (
159
+ <div>
160
+ {error && <p role="alert">{error}</p>}
161
+ <button onClick={pay} disabled={loading}>
162
+ {loading ? "Starting…" : "Pay with Paystack"}
163
+ </button>
164
+ </div>
165
+ );
166
+ }
167
+ ```
168
+
169
+ The same three SDK calls work in any frontend framework — only the
170
+ `@paystack/inline-js` popup call is Paystack-specific.
171
+
172
+ ### Redirect instead of inline
173
+
174
+ If you prefer a full-page redirect over the popup, skip `@paystack/inline-js`:
175
+ read `authorization_url` from the session `data`, send the browser there, and on
176
+ return to your callback page call `sdk.store.cart.complete(cartId)`.
177
+
178
+ > The server is always authoritative: `cart.complete` runs the provider's
179
+ > `authorizePayment`, which re-checks the transaction status, amount, and currency
180
+ > with Paystack before the order is created. Never treat the client `onSuccess` as
181
+ > proof of payment on its own.
182
+
183
+ ## Webhook
184
+
185
+ Create a webhook in your Paystack dashboard pointing at your Medusa server's payment
186
+ webhook endpoint for this provider:
187
+
188
+ ```
189
+ https://your-backend.com/hooks/payment/paystack_paystack
190
+ ```
191
+
192
+ The path segment is `<identifier>_<id>` — the provider's identifier (`paystack`)
193
+ joined with the `id` from your `medusa-config.ts`. With `id: "paystack"` the segment
194
+ is `paystack_paystack`; had you registered the provider with `id: "main"`, the path
195
+ would be `/hooks/payment/paystack_main`.
196
+
197
+ The provider verifies the `x-paystack-signature` (HMAC-SHA512 over the raw body using
198
+ your secret key) and captures the payment session on `charge.success`.
199
+
200
+ ## License
201
+
202
+ MIT
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "medusa-paystack",
3
+ "version": "0.1.0",
4
+ "description": "Paystack payment provider for Medusa v2",
5
+ "author": "nuette",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "medusa-plugin",
9
+ "medusa-plugin-payment",
10
+ "medusa-v2",
11
+ "paystack",
12
+ "payment"
13
+ ],
14
+ "files": [
15
+ ".medusa/server",
16
+ "package.json",
17
+ "README.md",
18
+ "AGENTS.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "medusa plugin:build",
23
+ "dev": "medusa plugin:develop",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "jest",
26
+ "prepublishOnly": "medusa plugin:build"
27
+ },
28
+ "exports": {
29
+ "./package.json": "./package.json",
30
+ "./providers/*": "./.medusa/server/src/providers/*/index.js",
31
+ "./*": "./.medusa/server/src/*.js"
32
+ },
33
+ "devDependencies": {
34
+ "@medusajs/admin-sdk": "2.16.0",
35
+ "@medusajs/cli": "2.16.0",
36
+ "@medusajs/framework": "2.16.0",
37
+ "@medusajs/medusa": "2.16.0",
38
+ "@medusajs/test-utils": "2.16.0",
39
+ "@swc/core": "1.5.7",
40
+ "@swc/jest": "^0.2.36",
41
+ "@types/jest": "^29.5.12",
42
+ "@types/node": "^20.11.0",
43
+ "jest": "^29.7.0",
44
+ "typescript": "^5.6.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@medusajs/framework": "2.16.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ }
52
+ }