Skip to content

Commit

Permalink
src/vault: Add possibility to decrement many vaults as well
Browse files Browse the repository at this point in the history
In future situations, we might need to decrement many vaults at once as well,(e.g. payment refund etc..)

- Added some unit tests to cover vaults updates
-Did some refactoring on commonly used types, as well as test mocks
  • Loading branch information
sashko9807 committed Feb 29, 2024
1 parent e61b2ec commit 7e37d19
Show file tree
Hide file tree
Showing 17 changed files with 288 additions and 129 deletions.
64 changes: 28 additions & 36 deletions apps/api/src/affiliate/affiliate.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,26 @@ import { VaultService } from '../vault/vault.service'
import { NotificationModule } from '../sockets/notifications/notification.module'
import { MarketingNotificationsModule } from '../notifications/notifications.module'
import { ExportService } from '../export/export.service'
import { Affiliate, Campaign, CampaignState, Donation, Prisma, Vault } from '@prisma/client'
import {
Affiliate,
AffiliateStatus,
Campaign,
CampaignState,
PaymentStatus,
Payments,
Prisma,
Vault,
} from '@prisma/client'
import { KeycloakTokenParsed } from '../auth/keycloak'
import { BadRequestException, ConflictException, ForbiddenException } from '@nestjs/common'
import { AffiliateStatusUpdateDto } from './dto/affiliate-status-update.dto'
import * as afCodeGenerator from './utils/affiliateCodeGenerator'
import { CreateAffiliateDonationDto } from './dto/create-affiliate-donation.dto'
import { mockPayment } from '../donations/__mocks__/paymentMock'

type PersonWithPayload = Prisma.PersonGetPayload<{ include: { company: true } }>
type AffiliateWithPayload = Prisma.AffiliateGetPayload<{
include: { company: { include: { person: true } }; donations: true }
include: { company: { include: { person: true } }; payments: true }
}>

describe('AffiliateController', () => {
Expand Down Expand Up @@ -126,28 +136,10 @@ describe('AffiliateController', () => {
countryCode: null,
person: { ...mockIndividualProfile },
},
donations: [],
payments: [],
}

const donationResponseMock: Donation = {
id: 'donation-id',
type: 'donation',
status: 'guaranteed',
amount: 5000,
affiliateId: activeAffiliateMock.id,
personId: null,
extCustomerId: '',
extPaymentIntentId: '123456',
extPaymentMethodId: '1234',
billingEmail: '[email protected]',
billingName: 'John Doe',
targetVaultId: vaultMock.id,
chargedAmount: 0,
currency: 'BGN',
createdAt: new Date(),
updatedAt: new Date(),
provider: 'bank',
}
const mockGuaranteedPayment = { ...mockPayment, status: PaymentStatus.guaranteed }

const userMock = {
sub: 'testKeycloackId',
Expand Down Expand Up @@ -232,13 +224,13 @@ describe('AffiliateController', () => {

const activeAffiliateMock: Affiliate = {
...affiliateMock,
status: 'active',
status: AffiliateStatus.active,
id: '12345',
affiliateCode: affiliateCodeMock,
}

const mockCancelledStatus: AffiliateStatusUpdateDto = {
newStatus: 'cancelled',
newStatus: AffiliateStatus.cancelled,
}
jest.spyOn(service, 'findOneById').mockResolvedValue(activeAffiliateMock)

Expand All @@ -265,7 +257,7 @@ describe('AffiliateController', () => {
jest.spyOn(service, 'findOneById').mockResolvedValue(activeAffiliateMock)

const updateStatusDto: AffiliateStatusUpdateDto = {
newStatus: 'active',
newStatus: AffiliateStatus.active,
}
const codeGenerationSpy = jest
.spyOn(afCodeGenerator, 'affiliateCodeGenerator')
Expand Down Expand Up @@ -305,7 +297,7 @@ describe('AffiliateController', () => {
jest.spyOn(service, 'findOneByCode').mockResolvedValue(activeAffiliateMock)
const createAffiliateDonationSpy = jest
.spyOn(donationService, 'createAffiliateDonation')
.mockResolvedValue(donationResponseMock)
.mockResolvedValue(mockGuaranteedPayment)
jest.spyOn(prismaMock.vault, 'findMany').mockResolvedValue([vaultMock])
prismaMock.campaign.findFirst.mockResolvedValue({
id: '123',
Expand All @@ -318,34 +310,34 @@ describe('AffiliateController', () => {
affiliateId: activeAffiliateMock.id,
})
expect(await donationService.createAffiliateDonation(affiliateDonationDto)).toEqual(
donationResponseMock,
mockGuaranteedPayment,
)
})
it('should cancel', async () => {
const cancelledDonationResponse: Donation = {
...donationResponseMock,
status: 'cancelled',
const cancelledDonationResponse: Payments = {
...mockGuaranteedPayment,
status: PaymentStatus.cancelled,
}
jest
.spyOn(donationService, 'getAffiliateDonationById')
.mockResolvedValue(donationResponseMock)
.mockResolvedValue(mockGuaranteedPayment)
jest.spyOn(donationService, 'update').mockResolvedValue(cancelledDonationResponse)
expect(
await controller.cancelAffiliateDonation(affiliateCodeMock, donationResponseMock.id),
await controller.cancelAffiliateDonation(affiliateCodeMock, mockGuaranteedPayment.id),
).toEqual(cancelledDonationResponse)
})
it('should throw error if donation status is succeeded', async () => {
const succeededDonationResponse: Donation = {
...donationResponseMock,
status: 'succeeded',
const succeededDonationResponse: Payments = {
...mockGuaranteedPayment,
status: PaymentStatus.succeeded,
}

jest
.spyOn(donationService, 'getAffiliateDonationById')
.mockResolvedValue(succeededDonationResponse)
const updateDonationStatus = jest.spyOn(donationService, 'update')
expect(
controller.cancelAffiliateDonation(affiliateCodeMock, donationResponseMock.id),
controller.cancelAffiliateDonation(affiliateCodeMock, mockGuaranteedPayment.id),
).rejects.toThrow(new BadRequestException("Donation status can't be updated"))
expect(updateDonationStatus).not.toHaveBeenCalled()
})
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/affiliate/types/affiliate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Prisma } from '@prisma/client'

export type AffiliateWithDonation = Prisma.AffiliateGetPayload<{
include: { payments: { include: { donations: true } } }
}>

export type AffiliateWithCompanyPayload = Prisma.AffiliateGetPayload<{
include: { company: { include: { person: true } }; donations: true }
}>
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export class BankTransactionsService {
matchedRef: newPaymentRef,
},
})
console.log(`called`)

// Import Donation
await this.donationService.createUpdateBankPayment(bankPayment)
})
Expand Down
38 changes: 11 additions & 27 deletions apps/api/src/campaign/campaign.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@ import { MarketingNotificationsService } from '../notifications/notifications.se
import { EmailService } from '../email/email.service'
import { TemplateService } from '../email/template.service'
import { CampaignNewsService } from '../campaign-news/campaign-news.service'
import { personMock } from '../person/__mock__/peronMock'

describe('CampaignController', () => {
let controller: CampaignController
let prismaService: PrismaService
let campaignService: CampaignService
let marketingProvider: NotificationsProviderInterface<unknown>
let marketingProvider: NotificationsProviderInterface
let marketingService: MarketingNotificationsService
const personServiceMock = {
findOneByKeycloakId: jest.fn(() => {
return { id: personIdMock }
}),
findByEmail: jest.fn(async () => {
findByEmail: jest.fn(async (): Promise<Person | null> => {
return person
}),
update: jest.fn(async () => {
Expand All @@ -51,24 +52,7 @@ describe('CampaignController', () => {
}),
}

const person: Person | null = {
id: 'e43348aa-be33-4c12-80bf-2adfbf8736cd',
firstName: 'John',
lastName: 'Doe',
keycloakId: 'some-id',
email: '[email protected]',
emailConfirmed: false,
companyId: null,
phone: null,
picture: null,
createdAt: new Date('2021-10-07T13:38:11.097Z'),
updatedAt: new Date('2021-10-07T13:38:11.097Z'),
newsletter: true,
address: null,
birthday: null,
personalNumber: null,
stripeCustomerId: null,
}
const person: Person = personMock

const mockCreateCampaign = {
slug: 'test-slug',
Expand Down Expand Up @@ -428,7 +412,7 @@ describe('CampaignController', () => {
describe('subscribeToCampaignNotifications', () => {
it('should throw if no consent is provided', async () => {
const data: CampaignSubscribeDto = {
email: person.email,
email: person.email!,
consent: false,
}

Expand All @@ -439,7 +423,7 @@ describe('CampaignController', () => {

it('should throw if the campaign is not active', async () => {
const data: CampaignSubscribeDto = {
email: person.email,
email: person.email!,
consent: true,
}

Expand All @@ -458,7 +442,7 @@ describe('CampaignController', () => {
jest.spyOn(marketingService, 'sendConfirmEmail')

const data: CampaignSubscribeDto = {
email: person.email,
email: person.email!,
// Valid consent
consent: true,
}
Expand Down Expand Up @@ -497,7 +481,7 @@ describe('CampaignController', () => {
jest.spyOn(campaignService, 'createCampaignNotificationList')

const data: CampaignSubscribeDto = {
email: person.email,
email: person.email!,
// Valid consent
consent: true,
}
Expand Down Expand Up @@ -538,7 +522,7 @@ describe('CampaignController', () => {
jest.spyOn(marketingService, 'sendConfirmEmail')

const data: CampaignSubscribeDto = {
email: person.email,
email: person.email!,
// Valid consent
consent: true,
}
Expand Down Expand Up @@ -566,7 +550,7 @@ describe('CampaignController', () => {
expect(marketingService.sendConfirmEmail).not.toHaveBeenCalled()
})

it('should create a saparate notification consent record for non-registered emails', async () => {
it('should create a separate notification consent record for non-registered emails', async () => {
jest.spyOn(marketingProvider, 'addContactsToList').mockImplementation(async () => '')
jest.spyOn(campaignService, 'createCampaignNotificationList')
jest.spyOn(marketingService, 'sendConfirmEmail')
Expand All @@ -577,7 +561,7 @@ describe('CampaignController', () => {
})

const data: CampaignSubscribeDto = {
email: person.email,
email: person.email!,
// Valid consent
consent: true,
}
Expand Down
16 changes: 8 additions & 8 deletions apps/api/src/campaign/campaign.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,15 +624,15 @@ export class CampaignService {

private async updateDonationIfAllowed(
tx: Prisma.TransactionClient,
donation: Prisma.PaymentsGetPayload<{ include: { donations: true } }>,
payment: Prisma.PaymentsGetPayload<{ include: { donations: true } }>,
newDonationStatus: PaymentStatus,
paymentData: PaymentData,
) {
if (shouldAllowStatusChange(donation.status, newDonationStatus)) {
if (shouldAllowStatusChange(payment.status, newDonationStatus)) {
try {
const updatedDonation = await tx.payments.update({
where: {
id: donation.id,
id: payment.id,
},
data: {
status: newDonationStatus,
Expand All @@ -648,11 +648,11 @@ export class CampaignService {

//if donation is switching to successful, increment the vault amount and send notification
if (
donation.status != PaymentStatus.succeeded &&
payment.status != PaymentStatus.succeeded &&
newDonationStatus === PaymentStatus.succeeded
) {
await this.vaultService.incrementVaultAmount(
donation.donations[0].targetVaultId,
payment.donations[0].targetVaultId,
paymentData.netAmount,
tx,
)
Expand All @@ -661,11 +661,11 @@ export class CampaignService {
person: updatedDonation.donations[0].person,
})
} else if (
donation.status === PaymentStatus.succeeded &&
payment.status === PaymentStatus.succeeded &&
newDonationStatus === PaymentStatus.refund
) {
await this.vaultService.decrementVaultAmount(
donation.donations[0].targetVaultId,
payment.donations[0].targetVaultId,
paymentData.netAmount,
tx,
)
Expand All @@ -686,7 +686,7 @@ export class CampaignService {
else {
Logger.warn(
`Skipping update of donation with paymentIntentId: ${paymentData.paymentIntentId}
and status: ${newDonationStatus} because the event comes after existing donation with status: ${donation.status}`,
and status: ${newDonationStatus} because the event comes after existing donation with status: ${payment.status}`,
)
}
}
Expand Down
57 changes: 57 additions & 0 deletions apps/api/src/donations/__mocks__/paymentMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Currency, DonationType, PaymentProvider, PaymentStatus } from '@prisma/client'
import { PaymentWithDonation } from '../types/donation'
import { DonationWithPerson } from '../types/donation'
import { personMock } from '../../person/__mock__/peronMock'

export const mockDonation: DonationWithPerson = {
id: '1234',
paymentId: '123',
type: DonationType.donation,
amount: 10,
targetVaultId: 'vault-1',
createdAt: new Date('2022-01-01'),
updatedAt: new Date('2022-01-02'),
personId: '1',
person: personMock,
}

//Mock donation to different vault
const mockDonationWithDiffVaultId: DonationWithPerson = {
...mockDonation,
targetVaultId: 'vault-2',
}

//Mock donation to same vault as mockDonation, but different amount
const mockDonationWithDiffAmount: DonationWithPerson = { ...mockDonation, amount: 50 }

export const mockPayment: PaymentWithDonation = {
id: '123',
provider: PaymentProvider.bank,
currency: Currency.BGN,
type: 'single',
status: PaymentStatus.initial,
amount: 10,
affiliateId: null,
extCustomerId: 'hahaha',
extPaymentIntentId: 'pm1',
extPaymentMethodId: 'bank',
billingEmail: '[email protected]',
billingName: 'Test',
chargedAmount: 10.5,
createdAt: new Date('2022-01-01'),
updatedAt: new Date('2022-01-02'),
donations: [mockDonation, mockDonationWithDiffVaultId, mockDonationWithDiffAmount],
}

export const mockSucceededPayment: PaymentWithDonation = {
...mockPayment,
status: PaymentStatus.succeeded,
}
export const mockGuaranteedPayment: PaymentWithDonation = {
...mockPayment,
status: PaymentStatus.guaranteed,
}
export const mockCancelledPayment: PaymentWithDonation = {
...mockPayment,
status: PaymentStatus.cancelled,
}
Loading

0 comments on commit 7e37d19

Please sign in to comment.