Skip to content

Commit

Permalink
Handle payment_intent.payment_failed when card is not accepted (#550)
Browse files Browse the repository at this point in the history
* Handle payment_intent.payment_failed stripewebhook

* Add unit tests

* Fix test
  • Loading branch information
Nnachevvv authored Sep 19, 2023
1 parent 4bcec53 commit 8fc54d1
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 6 deletions.
37 changes: 37 additions & 0 deletions apps/api/src/donations/events/stripe-payment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -198,6 +199,42 @@ 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>(CampaignService)
const mockedCampaignById = jest
.spyOn(campaignService, 'getCampaignById')
.mockImplementation(() => Promise.resolve(mockedCampaign))

const paymentData = getPaymentData(mockPaymentEventFailed.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'
Expand Down
32 changes: 26 additions & 6 deletions apps/api/src/donations/events/stripe-payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
string2RecurringDonationStatus,
getInvoiceData,
getPaymentDataFromCharge,
PaymentData,
} from '../helpers/payment-intent-helpers'
import { DonationStatus, CampaignState } from '@prisma/client'

Expand Down Expand Up @@ -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(
Expand All @@ -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')
Expand Down
74 changes: 74 additions & 0 deletions apps/api/src/donations/events/stripe-payment.testdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 8fc54d1

Please sign in to comment.