From 3e1207abf6b60fddbd5b82b0b76654f6e317080a Mon Sep 17 00:00:00 2001 From: Nnachevvv Date: Sun, 17 Sep 2023 23:55:12 +0300 Subject: [PATCH 1/3] Handle payment_intent.payment_failed stripewebhook --- .../events/stripe-payment.service.ts | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/api/src/donations/events/stripe-payment.service.ts b/apps/api/src/donations/events/stripe-payment.service.ts index 6658ea241..77c8b27ab 100644 --- a/apps/api/src/donations/events/stripe-payment.service.ts +++ b/apps/api/src/donations/events/stripe-payment.service.ts @@ -13,6 +13,7 @@ import { string2RecurringDonationStatus, getInvoiceData, getPaymentDataFromCharge, + PaymentData, } from '../helpers/payment-intent-helpers' import { DonationStatus, CampaignState } from '@prisma/client' @@ -69,6 +70,30 @@ export class StripePaymentService { paymentIntent.metadata as DonationMetadata, ) + const billingData = getPaymentData(paymentIntent) + + this.updatePaymentDonationStatus(paymentIntent, billingData, DonationStatus.cancelled) + } + + @StripeWebhookHandler('payment_intent.payment_failed') + async handlePaymentIntentFailed(event: Stripe.Event) { + const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent + Logger.log( + '[ handlePaymentIntentFailed ]', + paymentIntent, + paymentIntent.metadata as DonationMetadata, + ) + + const billingData = getPaymentData(paymentIntent) + + await this.updatePaymentDonationStatus(paymentIntent, billingData, DonationStatus.declined) + } + + async updatePaymentDonationStatus( + paymentIntent: Stripe.PaymentIntent, + billingData: PaymentData, + donationStatus: DonationStatus, + ) { const metadata: DonationMetadata = paymentIntent.metadata as DonationMetadata if (!metadata.campaignId) { throw new BadRequestException( @@ -79,12 +104,7 @@ export class StripePaymentService { const campaign = await this.campaignService.getCampaignById(metadata.campaignId) - const billingData = getPaymentData(paymentIntent) - await this.campaignService.updateDonationPayment( - campaign, - billingData, - DonationStatus.cancelled, - ) + await this.campaignService.updateDonationPayment(campaign, billingData, donationStatus) } @StripeWebhookHandler('charge.succeeded') From 975a7dfe41771492b8519e195cd586a00d5b4143 Mon Sep 17 00:00:00 2001 From: Nnachevvv Date: Sun, 17 Sep 2023 23:55:25 +0300 Subject: [PATCH 2/3] Add unit tests --- .../events/stripe-payment.service.spec.ts | 39 ++++++++++ .../events/stripe-payment.testdata.ts | 74 +++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/apps/api/src/donations/events/stripe-payment.service.spec.ts b/apps/api/src/donations/events/stripe-payment.service.spec.ts index 48988c2c1..87b152a28 100644 --- a/apps/api/src/donations/events/stripe-payment.service.spec.ts +++ b/apps/api/src/donations/events/stripe-payment.service.spec.ts @@ -36,6 +36,7 @@ import { mockedCampaignCompeleted, mockedVault, mockChargeEventSucceeded, + mockPaymentEventFailed, } from './stripe-payment.testdata' import { DonationStatus } from '@prisma/client' import { RecurringDonationService } from '../../recurring-donation/recurring-donation.service' @@ -198,6 +199,44 @@ describe('StripePaymentService', () => { }) }) + it('should handle payment_intent.payment_failed', () => { + const payloadString = JSON.stringify(mockPaymentEventFailed, null, 2) + + const header = stripe.webhooks.generateTestHeaderString({ + payload: payloadString, + secret: stripeSecret, + }) + + const campaignService = app.get(CampaignService) + const mockedCampaignById = jest + .spyOn(campaignService, 'getCampaignById') + .mockImplementation(() => Promise.resolve(mockedCampaign)) + + const paymentData = getPaymentData( + mockPaymentEventCancelled.data.object as Stripe.PaymentIntent, + ) + + const mockedUpdateDonationPayment = jest + .spyOn(campaignService, 'updateDonationPayment') + .mockImplementation(() => Promise.resolve('')) + .mockName('updateDonationPayment') + + return request(app.getHttpServer()) + .post(defaultStripeWebhookEndpoint) + .set('stripe-signature', header) + .type('json') + .send(payloadString) + .expect(201) + .then(() => { + expect(mockedCampaignById).toHaveBeenCalledWith(campaignId) //campaignId from the Stripe Event + expect(mockedUpdateDonationPayment).toHaveBeenCalledWith( + mockedCampaign, + paymentData, + DonationStatus.declined, + ) + }) + }) + it('should handle charge.succeeded for not anonymous user', () => { //Set not anonymous explicitly in the test data ;(mockChargeEventSucceeded.data.object as Stripe.Charge).metadata.isAnonymous = 'false' diff --git a/apps/api/src/donations/events/stripe-payment.testdata.ts b/apps/api/src/donations/events/stripe-payment.testdata.ts index 0f01525a2..423048da3 100644 --- a/apps/api/src/donations/events/stripe-payment.testdata.ts +++ b/apps/api/src/donations/events/stripe-payment.testdata.ts @@ -206,6 +206,80 @@ export const mockPaymentEventCancelled: Stripe.Event = { type: 'payment_intent.canceled', } +export const mockPaymentEventFailed: Stripe.Event = { + id: 'evt_3LUzB4KApGjVGa9t0lyGsAk8', + object: 'event', + api_version: '2020-08-27', + created: 1660163846, + data: { + object: { + id: 'pi_3LUzB4KApGjVGa9t0NGvE94K', + object: 'payment_intent', + amount: 1065, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 0, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [], + has_more: false, + total_count: 0, + url: '/v1/charges?payment_intent=pi_3LUzB4KApGjVGa9t0NGvE94K', + }, + client_secret: 'pi', + confirmation_method: 'automatic', + created: 1660077446, + currency: 'bgn', + customer: null, + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: campaignId, + }, + next_action: null, + on_behalf_of: null, + payment_method: null, + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: null, + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'payment_failed', + transfer_data: null, + transfer_group: null, + }, + }, + livemode: false, + pending_webhooks: 1, + request: { + id: null, + idempotency_key: '8a2367c4-45bc-40a0-86c0-cff4c2229bec', + }, + type: 'payment_intent.payment_failed', +} + export const mockChargeEventSucceeded: Stripe.Event = { id: 'evt_3MmYVtKApGjVGa9t1d6zrtYm', object: 'event', From cf02afa5a8a7a0ebf4cfa6cbbe4268ede90c7665 Mon Sep 17 00:00:00 2001 From: Nnachevvv Date: Mon, 18 Sep 2023 00:11:15 +0300 Subject: [PATCH 3/3] Fix test --- apps/api/src/donations/events/stripe-payment.service.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/api/src/donations/events/stripe-payment.service.spec.ts b/apps/api/src/donations/events/stripe-payment.service.spec.ts index 87b152a28..07f4a997b 100644 --- a/apps/api/src/donations/events/stripe-payment.service.spec.ts +++ b/apps/api/src/donations/events/stripe-payment.service.spec.ts @@ -212,9 +212,7 @@ describe('StripePaymentService', () => { .spyOn(campaignService, 'getCampaignById') .mockImplementation(() => Promise.resolve(mockedCampaign)) - const paymentData = getPaymentData( - mockPaymentEventCancelled.data.object as Stripe.PaymentIntent, - ) + const paymentData = getPaymentData(mockPaymentEventFailed.data.object as Stripe.PaymentIntent) const mockedUpdateDonationPayment = jest .spyOn(campaignService, 'updateDonationPayment')