From afcb2c67ea2cbef053c1919fa2faa02253e3495b Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Mon, 28 Aug 2023 21:42:06 +0000 Subject: [PATCH 1/9] feat: add debug option with logs --- .../services/paystack-payment-processor.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/plugin/src/services/paystack-payment-processor.ts b/packages/plugin/src/services/paystack-payment-processor.ts index 7fe8f39..b210106 100644 --- a/packages/plugin/src/services/paystack-payment-processor.ts +++ b/packages/plugin/src/services/paystack-payment-processor.ts @@ -22,6 +22,8 @@ export interface PaystackPaymentProcessorConfig { * https://dashboard.paystack.com/#/settings/developers */ secret_key: string; + + debug?: boolean; } class PaystackPaymentProcessor extends AbstractPaymentProcessor { @@ -29,6 +31,7 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { protected readonly configuration: PaystackPaymentProcessorConfig; protected readonly paystack: Paystack; + protected readonly debug: boolean; constructor( container: MedusaContainer, @@ -45,6 +48,7 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { this.configuration = options; this.paystack = new Paystack(this.configuration.secret_key); + this.debug = Boolean(options.debug); } get paymentIntentOptions() { @@ -63,6 +67,13 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { }; }) > { + if (this.debug) { + console.info( + "PS_P_Debug: InitiatePayment", + JSON.stringify(context, null, 2), + ); + } + const { amount, email, currency_code } = context; const validatedCurrencyCode = validateCurrencyCode(currency_code); @@ -97,6 +108,13 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { ): Promise< PaymentProcessorSessionResponse["session_data"] | PaymentProcessorError > { + if (this.debug) { + console.info( + "PS_P_Debug: UpdatePaymentData", + JSON.stringify({ _, data }, null, 2), + ); + } + if (data.amount) { throw new MedusaError( MedusaErrorTypes.INVALID_DATA, @@ -122,6 +140,13 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { }; }) > { + if (this.debug) { + console.info( + "PS_P_Debug: UpdatePayment", + JSON.stringify(context, null, 2), + ); + } + // Re-initialize the payment return this.initiatePayment(context); } @@ -139,6 +164,13 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { data: Record; } > { + if (this.debug) { + console.info( + "PS_P_Debug: AuthorizePayment", + JSON.stringify(paymentSessionData, null, 2), + ); + } + try { const { paystackTxRef } = paymentSessionData; @@ -146,6 +178,16 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { reference: paystackTxRef, }); + if (this.debug) { + console.info( + "PS_P_Debug: AuthorizePayment: Verification", + JSON.stringify({ status, data }, null, 2), + ); + } + + // TODO: Verify currency + // TODO: Verify amount + if (status === false) { // Invalid key error return { @@ -198,6 +240,13 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { async retrievePayment( paymentSessionData: Record & { paystackTxId: string }, ): Promise | PaymentProcessorError> { + if (this.debug) { + console.info( + "PS_P_Debug: RetrievePayment", + JSON.stringify(paymentSessionData, null, 2), + ); + } + try { const { paystackTxId } = paymentSessionData; @@ -227,6 +276,13 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { paymentSessionData: Record, refundAmount: number, ): Promise | PaymentProcessorError> { + if (this.debug) { + console.info( + "PS_P_Debug: RefundPayment", + JSON.stringify({ paymentSessionData, refundAmount }, null, 2), + ); + } + try { const { paystackTxId } = paymentSessionData; @@ -256,6 +312,13 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { async getPaymentStatus( paymentSessionData: Record & { paystackTxId?: string }, ): Promise { + if (this.debug) { + console.info( + "PS_P_Debug: GetPaymentStatus", + JSON.stringify(paymentSessionData, null, 2), + ); + } + const { paystackTxId } = paymentSessionData; if (!paystackTxId) { From 44b1ea4d50834118c8bdb233a6e2c6508a0b2f56 Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Mon, 28 Aug 2023 21:42:36 +0000 Subject: [PATCH 2/9] fix: remove amount redenomination --- packages/plugin/src/services/paystack-payment-processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/src/services/paystack-payment-processor.ts b/packages/plugin/src/services/paystack-payment-processor.ts index b210106..dc7afbb 100644 --- a/packages/plugin/src/services/paystack-payment-processor.ts +++ b/packages/plugin/src/services/paystack-payment-processor.ts @@ -80,7 +80,7 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { const { data, status, message } = await this.paystack.transaction.initialize({ - amount: amount * 100, // Paystack expects amount in lowest denomination - https://paystack.com/docs/payments/accept-payments/#initialize-transaction-1 + amount: amount, // Paystack expects amount in lowest denomination - https://paystack.com/docs/payments/accept-payments/#initialize-transaction-1 email, currency: validatedCurrencyCode, }); From 5791d949175859fd765025c084d7c07f96ed7295 Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Mon, 28 Aug 2023 21:44:35 +0000 Subject: [PATCH 3/9] chore: add option description --- packages/plugin/src/services/paystack-payment-processor.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/plugin/src/services/paystack-payment-processor.ts b/packages/plugin/src/services/paystack-payment-processor.ts index dc7afbb..628f653 100644 --- a/packages/plugin/src/services/paystack-payment-processor.ts +++ b/packages/plugin/src/services/paystack-payment-processor.ts @@ -23,6 +23,11 @@ export interface PaystackPaymentProcessorConfig { */ secret_key: string; + /** + * Debug mode + * If true, logs helpful debug information to the console + * Logs are prefixed with "PS_P_Debug" + */ debug?: boolean; } From a916d992be6fe8add00a9abb1c8644265b03112c Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Wed, 30 Aug 2023 01:03:01 +0000 Subject: [PATCH 4/9] fix: remove ds_store --- .gitignore | 3 ++- packages/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 packages/.DS_Store diff --git a/.gitignore b/.gitignore index 3091757..6d3a831 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -coverage \ No newline at end of file +coverage +.DS_Store \ No newline at end of file diff --git a/packages/.DS_Store b/packages/.DS_Store deleted file mode 100644 index 3755b355507557a20633066d019b86ec91266b47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5Z-NTn^J@v6!f;>wP4z+C|*LWFJMFuDm5Xc24l7~tvQrJ?)pN$h|lB9 z?&c6IcoVTRu=~x<&u->}>6V7|z_V0MS2Yblek z)PwLQ9_J%_=R&6QAWp|Kl@Lc`2)VzF(@5r?T%=K^a((Tv+E#mH?=P1<=k&NM2K~XR zE0$+{rz?&IC#zN4+C4ZtzZyL!FR6UfOmZMy$+p1~-a)Ap_3F>kM5Yt4mpNrDAu&J< z5Cg=(#xY=y0;{uel~cjQ05R|r1Gqm3Xo#-CLZjL`pu_7k`WuKSpyOKtQ5bX$78=0= z!gVU3PUYr_!F4*=g^6_UYz?rNl-7$63!4Ak||#`FIY zewnq8{Avn~hyh~YpE1B2Lx1Q&QRZy@Rvwo34j6aBYVoJ{Q`A} Ya}5?6aTc_zbU?ZYC_< Date: Wed, 30 Aug 2023 02:06:40 +0000 Subject: [PATCH 5/9] feat: add currency and amount verification --- .gitignore | 3 +- packages/plugin/src/lib/paystack.ts | 2 ++ .../services/paystack-payment-processor.ts | 32 +++++++++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 6d3a831..77489ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules coverage -.DS_Store \ No newline at end of file +.DS_Store +turbo-build.log \ No newline at end of file diff --git a/packages/plugin/src/lib/paystack.ts b/packages/plugin/src/lib/paystack.ts index e5e0529..e0b0a29 100644 --- a/packages/plugin/src/lib/paystack.ts +++ b/packages/plugin/src/lib/paystack.ts @@ -91,6 +91,8 @@ export default class Paystack { id: number; status: string; reference: string; + amount: number; + currency: string; }> >({ path: "/transaction/verify/" + reference, diff --git a/packages/plugin/src/services/paystack-payment-processor.ts b/packages/plugin/src/services/paystack-payment-processor.ts index 628f653..4387906 100644 --- a/packages/plugin/src/services/paystack-payment-processor.ts +++ b/packages/plugin/src/services/paystack-payment-processor.ts @@ -7,6 +7,7 @@ import { PaymentProcessorSessionResponse, PaymentSessionStatus, MedusaContainer, + CartService, } from "@medusajs/medusa"; import { MedusaError, MedusaErrorTypes } from "@medusajs/utils"; import { validateCurrencyCode } from "../utils/currencyCode"; @@ -34,6 +35,7 @@ export interface PaystackPaymentProcessorConfig { class PaystackPaymentProcessor extends AbstractPaymentProcessor { static identifier = "paystack"; + protected readonly cartService: CartService; protected readonly configuration: PaystackPaymentProcessorConfig; protected readonly paystack: Paystack; protected readonly debug: boolean; @@ -54,6 +56,16 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { this.configuration = options; this.paystack = new Paystack(this.configuration.secret_key); this.debug = Boolean(options.debug); + + // @ts-expect-error - Container is just an object - https://docs.medusajs.com/development/fundamentals/dependency-injection#in-classes + this.cartService = container.cartService; + + if (this.cartService.retrieveWithTotals === undefined) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + "Your Medusa installation contains an outdated cartService implementation. Update your Medusa installation.", + ); + } } get paymentIntentOptions() { @@ -69,6 +81,7 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { session_data: { paystackTxRef: string; paystackTxAuthData: PaystackTransactionAuthorisation; + cartId: string; }; }) > { @@ -100,6 +113,7 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { session_data: { paystackTxRef: data.reference, paystackTxAuthData: data, + cartId: context.resource_id, }, }; } @@ -161,7 +175,10 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { * We validate the payment and return a status */ async authorizePayment( - paymentSessionData: Record & { paystackTxRef: string }, + paymentSessionData: Record & { + paystackTxRef: string; + cartId: string; + }, ): Promise< | PaymentProcessorError | { @@ -177,23 +194,26 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { } try { - const { paystackTxRef } = paymentSessionData; + const { paystackTxRef, cartId } = paymentSessionData; const { status, data } = await this.paystack.transaction.verify({ reference: paystackTxRef, }); + const cart = await this.cartService.retrieveWithTotals(cartId); + if (this.debug) { console.info( "PS_P_Debug: AuthorizePayment: Verification", - JSON.stringify({ status, data }, null, 2), + JSON.stringify({ status, cart, data }, null, 2), ); } - // TODO: Verify currency - // TODO: Verify amount + const amountValid = Math.round(cart.total) === Math.round(data.amount); + const currencyValid = + cart.region.currency_code === data.currency.toLowerCase(); - if (status === false) { + if (status === false || !amountValid || !currencyValid) { // Invalid key error return { status: PaymentSessionStatus.ERROR, From 01163de45d572c1968ebec043fb0e554789a317b Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Wed, 30 Aug 2023 12:21:29 +0000 Subject: [PATCH 6/9] fix: only check amount and currency on transactions considered successful --- .../services/paystack-payment-processor.ts | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/plugin/src/services/paystack-payment-processor.ts b/packages/plugin/src/services/paystack-payment-processor.ts index 4387906..c458db4 100644 --- a/packages/plugin/src/services/paystack-payment-processor.ts +++ b/packages/plugin/src/services/paystack-payment-processor.ts @@ -209,11 +209,7 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { ); } - const amountValid = Math.round(cart.total) === Math.round(data.amount); - const currencyValid = - cart.region.currency_code === data.currency.toLowerCase(); - - if (status === false || !amountValid || !currencyValid) { + if (status === false) { // Invalid key error return { status: PaymentSessionStatus.ERROR, @@ -226,15 +222,44 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { } switch (data.status) { - case "success": - // Successful transaction + case "success": { + const amountValid = + Math.round(cart.total) === Math.round(data.amount); + const currencyValid = + cart.region.currency_code === data.currency.toLowerCase(); + + if (amountValid && currencyValid) { + // Successful transaction + return { + status: PaymentSessionStatus.AUTHORIZED, + data: { + paystackTxId: data.id, + paystackTxData: data, + }, + }; + } + + // Invalid amount or currency + // We refund the transaction + await this.refundPayment( + { + ...paymentSessionData, + paystackTxData: data, + paystackTxId: data.id, + }, + data.amount, + ); + + // And return the failed status return { - status: PaymentSessionStatus.AUTHORIZED, + status: PaymentSessionStatus.ERROR, data: { + ...paymentSessionData, paystackTxId: data.id, paystackTxData: data, }, }; + } case "failed": // Failed transaction @@ -298,7 +323,7 @@ class PaystackPaymentProcessor extends AbstractPaymentProcessor { * Refunds payment for Paystack transaction */ async refundPayment( - paymentSessionData: Record, + paymentSessionData: Record & { paystackTxId: number }, refundAmount: number, ): Promise | PaymentProcessorError> { if (this.debug) { From 0ba4d93570ce18558ce9fbb9ab02ca1420a8d96a Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Wed, 30 Aug 2023 12:24:48 +0000 Subject: [PATCH 7/9] tests: add cases for differing paid amount --- packages/plugin/src/__mocks__/cart.ts | 16 ++++++++++ packages/plugin/src/lib/paystack.ts | 2 +- .../__tests__/paystack-payment-processor.ts | 30 +++++++++++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 packages/plugin/src/__mocks__/cart.ts diff --git a/packages/plugin/src/__mocks__/cart.ts b/packages/plugin/src/__mocks__/cart.ts new file mode 100644 index 0000000..aba1a15 --- /dev/null +++ b/packages/plugin/src/__mocks__/cart.ts @@ -0,0 +1,16 @@ +export const CartServiceMock = { + retrieveWithTotals: jest.fn().mockImplementation((cartId: string) => { + const amount = cartId === "cart-123" ? 2000 : 1000; + + return Promise.resolve({ + total: amount, + region: { + currency_code: "ghs", + }, + }); + }), +}; + +const mock = jest.fn().mockImplementation(() => CartServiceMock); + +export default mock; diff --git a/packages/plugin/src/lib/paystack.ts b/packages/plugin/src/lib/paystack.ts index e0b0a29..33aab31 100644 --- a/packages/plugin/src/lib/paystack.ts +++ b/packages/plugin/src/lib/paystack.ts @@ -143,7 +143,7 @@ export default class Paystack { transaction, amount, }: { - transaction: string; + transaction: number; amount: number; }) => this.requestPaystackAPI< diff --git a/packages/plugin/src/services/__tests__/paystack-payment-processor.ts b/packages/plugin/src/services/__tests__/paystack-payment-processor.ts index 83054b6..deeafc8 100644 --- a/packages/plugin/src/services/__tests__/paystack-payment-processor.ts +++ b/packages/plugin/src/services/__tests__/paystack-payment-processor.ts @@ -1,10 +1,12 @@ -import PaystackPaymentProcessor from "../paystack-payment-processor"; import { PaymentProcessorContext, PaymentProcessorError, PaymentSessionStatus, } from "@medusajs/medusa"; +import PaystackPaymentProcessor from "../paystack-payment-processor"; +import { CartServiceMock } from "../../__mocks__/cart"; + interface ProviderServiceMockOptions { secretKey?: string | undefined; } @@ -16,8 +18,10 @@ function createPaystackProviderService( }, ) { return new PaystackPaymentProcessor( - // @ts-expect-error - We don't need to mock all the methods - {}, + { + // @ts-expect-error - We don't need to mock all the methods + cartService: CartServiceMock, + }, { secret_key: secretKey, }, @@ -102,6 +106,7 @@ describe("Authorize Payment", () => { const payment = checkForPaymentProcessorError( await service.authorizePayment({ paystackTxRef: "123-failed", + cartId: "cart-123", }), ); expect(payment.status).toEqual(PaymentSessionStatus.ERROR); @@ -113,6 +118,7 @@ describe("Authorize Payment", () => { const payment = checkForPaymentProcessorError( await service.authorizePayment({ paystackTxRef: "123-passed", + cartId: "cart-123", }), ); @@ -124,6 +130,7 @@ describe("Authorize Payment", () => { const payment = checkForPaymentProcessorError( await service.authorizePayment({ paystackTxRef: "123-false", + cartId: "cart-123", }), ); @@ -136,11 +143,24 @@ describe("Authorize Payment", () => { const payment = checkForPaymentProcessorError( await service.authorizePayment({ paystackTxRef: "123-pending", + cartId: "cart-123", }), ); expect(payment.status).toEqual(PaymentSessionStatus.PENDING); }); + + it("errors out if the returned amount differs", async () => { + const service = createPaystackProviderService(); + const payment = checkForPaymentProcessorError( + await service.authorizePayment({ + paystackTxRef: "123-passed", + cartId: "cart-1000", + }), + ); + + expect(payment.status).toEqual(PaymentSessionStatus.ERROR); + }); }); describe("getStatus", () => { @@ -231,13 +251,13 @@ describe("refundPayment", () => { const service = createPaystackProviderService(); const payment = await service.refundPayment( { - paystackTxId: "paystackTx", + paystackTxId: 1244, }, 4000, ); expect(payment).toMatchObject({ - paystackTxId: "paystackTx", + paystackTxId: 1244, }); }); }); From 731da1d72c4ef0e322a7b0c7186ae1723109b7a3 Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Wed, 30 Aug 2023 12:26:18 +0000 Subject: [PATCH 8/9] chore: add changelog --- .changeset/hot-pianos-sneeze.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hot-pianos-sneeze.md diff --git a/.changeset/hot-pianos-sneeze.md b/.changeset/hot-pianos-sneeze.md new file mode 100644 index 0000000..26ed072 --- /dev/null +++ b/.changeset/hot-pianos-sneeze.md @@ -0,0 +1,5 @@ +--- +"medusa-payment-paystack": patch +--- + +Re-adds amount and currency checks From 3fec15ba083d613ad6318448714e48512d73e99f Mon Sep 17 00:00:00 2001 From: Andrew Glago Date: Wed, 30 Aug 2023 12:30:20 +0000 Subject: [PATCH 9/9] chore: add debug feature to changelog --- .changeset/hot-pianos-sneeze.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/hot-pianos-sneeze.md b/.changeset/hot-pianos-sneeze.md index 26ed072..99f7770 100644 --- a/.changeset/hot-pianos-sneeze.md +++ b/.changeset/hot-pianos-sneeze.md @@ -3,3 +3,5 @@ --- Re-adds amount and currency checks + +Adds new debug mode for testing