diff --git a/apps/api/src/campaign/campaign.service.ts b/apps/api/src/campaign/campaign.service.ts index 23dfd820b..284e51b2e 100644 --- a/apps/api/src/campaign/campaign.service.ts +++ b/apps/api/src/campaign/campaign.service.ts @@ -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, 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 8586aade3..48988c2c1 100644 --- a/apps/api/src/donations/events/stripe-payment.service.spec.ts +++ b/apps/api/src/donations/events/stripe-payment.service.spec.ts @@ -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, @@ -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(), @@ -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) @@ -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() @@ -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(), @@ -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) @@ -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() }) diff --git a/apps/api/src/donations/events/stripe-payment.service.ts b/apps/api/src/donations/events/stripe-payment.service.ts index 3a214299b..1e10046da 100644 --- a/apps/api/src/donations/events/stripe-payment.service.ts +++ b/apps/api/src/donations/events/stripe-payment.service.ts @@ -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)