Skip to content

Commit

Permalink
fix updating donation after charge succeeded event (#542)
Browse files Browse the repository at this point in the history
* fixed: handling of stripe.charge.succeeded requires reading the amount of the incoming event not from the found in the database. fixed the tests too.

* formatting

* updated tests for campaign target sum reached
  • Loading branch information
quantum-grit authored Sep 4, 2023
1 parent dda9c2e commit 5afa9fe
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 32 deletions.
6 changes: 5 additions & 1 deletion apps/api/src/campaign/campaign.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,11 @@ export class CampaignService {

//if donation is switching to successful, increment the vault amount and send notification
if (newDonationStatus === DonationStatus.succeeded) {
await this.vaultService.incrementVaultAmount(donation.targetVaultId, donation.amount, tx)
await this.vaultService.incrementVaultAmount(
donation.targetVaultId,
paymentData.netAmount,
tx,
)
this.notificationService.sendNotification('successfulDonation', {
...updatedDonation,
person: updatedDonation.person,
Expand Down
99 changes: 68 additions & 31 deletions apps/api/src/donations/events/stripe-payment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import { INestApplication } from '@nestjs/common'
import request from 'supertest'
import { StripeModule, StripeModuleConfig, StripePayloadService } from '@golevelup/nestjs-stripe'

import { Donation, DonationType, RecurringDonationStatus } from '@prisma/client'
import {
Campaign,
CampaignState,
Donation,
DonationType,
RecurringDonationStatus,
Vault,
} from '@prisma/client'

import {
campaignId,
Expand Down Expand Up @@ -218,17 +225,17 @@ describe('StripePaymentService', () => {
.mockName('createDonationWish')
.mockImplementation(() => Promise.resolve())

prismaMock.donation.create.mockResolvedValue({
prismaMock.donation.findUnique.mockResolvedValue({
id: 'test-donation-id',
type: DonationType.donation,
status: DonationStatus.succeeded,
status: DonationStatus.waiting,
provider: 'stripe',
extCustomerId: paymentData.stripeCustomerId ?? '',
extPaymentIntentId: paymentData.paymentIntentId,
extPaymentMethodId: 'card',
targetVaultId: 'test-vault-id',
amount: paymentData.netAmount,
chargedAmount: paymentData.netAmount,
amount: 0, //amount is 0 on donation created from payment-intent
chargedAmount: 0,
currency: 'BGN',
createdAt: new Date(),
updatedAt: new Date(),
Expand All @@ -237,14 +244,34 @@ describe('StripePaymentService', () => {
personId: 'donation-person',
})

prismaMock.donation.update.mockResolvedValue({
id: 'test-donation-id',
targetVaultId: 'test-vault-id',
amount: paymentData.netAmount,
status: 'succeeded',
person: { firstName: 'Full', lastName: 'Name' },
} as Donation & { person: unknown })

prismaMock.vault.update.mockResolvedValue({ campaignId: 'test-campaign' } as Vault)

prismaMock.campaign.findFirst.mockResolvedValue({
id: 'test-campaign',
state: CampaignState.active,
targetAmount: paymentData.netAmount,
vaults: [{ amount: paymentData.netAmount }],
} as unknown as Campaign)

jest.spyOn(prismaMock, '$transaction').mockImplementation((callback) => callback(prismaMock))
const mockedUpdateDonationPayment = jest
.spyOn(campaignService, 'updateDonationPayment')
.mockName('updateDonationPayment')

const mockedIncrementVaultAmount = jest
.spyOn(vaultService, 'incrementVaultAmount')
.mockImplementation()
const mockedIncrementVaultAmount = jest.spyOn(vaultService, 'incrementVaultAmount')

const mockedUpdateCampaignStatusIfTargetReached = jest.spyOn(
campaignService,
'updateCampaignStatusIfTargetReached',
)

return request(app.getHttpServer())
.post(defaultStripeWebhookEndpoint)
Expand All @@ -255,16 +282,17 @@ describe('StripePaymentService', () => {
.then(() => {
expect(mockedCampaignById).toHaveBeenCalledWith(campaignId) //campaignId from the Stripe Event
expect(mockedUpdateDonationPayment).toHaveBeenCalled()
expect(prismaMock.donation.create).toHaveBeenCalled()
expect(prismaMock.donation.findUnique).toHaveBeenCalled()
expect(prismaMock.donation.create).not.toHaveBeenCalled()
expect(mockedIncrementVaultAmount).toHaveBeenCalled()
expect(prismaMock.donation.update).toHaveBeenCalledWith({
where: { id: 'test-donation-id' },
expect(prismaMock.donation.update).toHaveBeenCalledTimes(2) //once for the amount and second time for assigning donation to the person
expect(mockedUpdateCampaignStatusIfTargetReached).toHaveBeenCalled()
expect(prismaMock.campaign.update).toHaveBeenCalledWith({
where: {
id: 'test-campaign',
},
data: {
person: {
connect: {
email: paymentData.billingEmail,
},
},
state: CampaignState.complete,
},
})
expect(mockedcreateDonationWish).toHaveBeenCalled()
Expand Down Expand Up @@ -293,31 +321,22 @@ describe('StripePaymentService', () => {
mockChargeEventSucceeded.data.object as Stripe.Charge,
)

jest.spyOn(prismaMock, '$transaction').mockImplementation((callback) => callback(prismaMock))
const mockedUpdateDonationPayment = jest
.spyOn(campaignService, 'updateDonationPayment')
.mockName('updateDonationPayment')

const mockedIncrementVaultAmount = jest
.spyOn(vaultService, 'incrementVaultAmount')
.mockImplementation()

const mockedcreateDonationWish = jest
.spyOn(campaignService, 'createDonationWish')
.mockName('createDonationWish')
.mockImplementation(() => Promise.resolve())

prismaMock.donation.create.mockResolvedValue({
prismaMock.donation.findUnique.mockResolvedValue({
id: 'test-donation-id',
type: DonationType.donation,
status: DonationStatus.succeeded,
status: DonationStatus.waiting,
provider: 'stripe',
extCustomerId: paymentData.stripeCustomerId ?? '',
extPaymentIntentId: paymentData.paymentIntentId,
extPaymentMethodId: 'card',
targetVaultId: 'test-vault-id',
amount: paymentData.netAmount,
chargedAmount: paymentData.netAmount,
amount: 0, //amount is 0 on donation created from payment-intent
chargedAmount: 0,
currency: 'BGN',
createdAt: new Date(),
updatedAt: new Date(),
Expand All @@ -326,6 +345,23 @@ describe('StripePaymentService', () => {
personId: 'donation-person',
})

prismaMock.donation.update.mockResolvedValue({
id: 'test-donation-id',
targetVaultId: 'test-vault-id',
amount: (mockInvoicePaidEvent.data.object as Stripe.Invoice).amount_paid,
status: 'succeeded',
person: { firstName: 'Full', lastName: 'Name' },
} as Donation & { person: unknown })

prismaMock.vault.update.mockResolvedValue({ campaignId: 'test-campaign' } as Vault)

jest.spyOn(prismaMock, '$transaction').mockImplementation((callback) => callback(prismaMock))
const mockedUpdateDonationPayment = jest
.spyOn(campaignService, 'updateDonationPayment')
.mockName('updateDonationPayment')

const mockedIncrementVaultAmount = jest.spyOn(vaultService, 'incrementVaultAmount')

return request(app.getHttpServer())
.post(defaultStripeWebhookEndpoint)
.set('stripe-signature', header)
Expand All @@ -335,8 +371,9 @@ describe('StripePaymentService', () => {
.then(() => {
expect(mockedCampaignById).toHaveBeenCalledWith(campaignId) //campaignId from the Stripe Event
expect(mockedUpdateDonationPayment).toHaveBeenCalled()
expect(prismaMock.donation.create).toHaveBeenCalled()
expect(prismaMock.donation.update).not.toHaveBeenCalled()
expect(prismaMock.donation.findUnique).toHaveBeenCalled()
expect(prismaMock.donation.create).not.toHaveBeenCalled()
expect(prismaMock.donation.update).toHaveBeenCalledOnce() //for the donation to succeeded
expect(mockedIncrementVaultAmount).toHaveBeenCalled()
expect(mockedcreateDonationWish).toHaveBeenCalled()
})
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/donations/events/stripe-payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export class StripePaymentService {
DonationStatus.succeeded,
metadata,
)

//updateDonationPayment will mark the campaign as completed if amount is reached
await this.checkForCompletedCampaign(metadata.campaignId)

Expand Down

0 comments on commit 5afa9fe

Please sign in to comment.