Skip to content

Commit 530d9b7

Browse files
authored
[1/2]: Overhaul of donations module (#604)
* Donation table structural changes - Payment table will save the information regarding the payment - Donation table will save information regarding the donation(e.g. campaign, wish etc..) * Initial API changes for new payment structure * Adjust existing tests to cover the restructure * src/vault: Add possibility to decrement many vaults as well 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 * schema.prisma: Rename Payments to Payment As the name of the model is used as a type, the acceptednaming convention is to name that type in singular way * Infer person.email as string in campaign.controller test cases Fixes TS related errors * src/bank-transactions: Check for admin privilleges regardless of env when simulating IRIS transactions * donations.service: Rename currentDonation to currentPayment in update fn
1 parent 051ba48 commit 530d9b7

File tree

58 files changed

+1398
-630
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1398
-630
lines changed

apps/api/src/affiliate/affiliate.controller.spec.ts

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,26 @@ import { VaultService } from '../vault/vault.service'
1111
import { NotificationModule } from '../sockets/notifications/notification.module'
1212
import { MarketingNotificationsModule } from '../notifications/notifications.module'
1313
import { ExportService } from '../export/export.service'
14-
import { Affiliate, Campaign, CampaignState, Donation, Prisma, Vault } from '@prisma/client'
14+
import {
15+
Affiliate,
16+
AffiliateStatus,
17+
Campaign,
18+
CampaignState,
19+
PaymentStatus,
20+
Payments,
21+
Prisma,
22+
Vault,
23+
} from '@prisma/client'
1524
import { KeycloakTokenParsed } from '../auth/keycloak'
1625
import { BadRequestException, ConflictException, ForbiddenException } from '@nestjs/common'
1726
import { AffiliateStatusUpdateDto } from './dto/affiliate-status-update.dto'
1827
import * as afCodeGenerator from './utils/affiliateCodeGenerator'
1928
import { CreateAffiliateDonationDto } from './dto/create-affiliate-donation.dto'
29+
import { mockPayment } from '../donations/__mocks__/paymentMock'
2030

2131
type PersonWithPayload = Prisma.PersonGetPayload<{ include: { company: true } }>
2232
type AffiliateWithPayload = Prisma.AffiliateGetPayload<{
23-
include: { company: { include: { person: true } }; donations: true }
33+
include: { company: { include: { person: true } }; payments: true }
2434
}>
2535

2636
describe('AffiliateController', () => {
@@ -126,28 +136,10 @@ describe('AffiliateController', () => {
126136
countryCode: null,
127137
person: { ...mockIndividualProfile },
128138
},
129-
donations: [],
139+
payments: [],
130140
}
131141

132-
const donationResponseMock: Donation = {
133-
id: 'donation-id',
134-
type: 'donation',
135-
status: 'guaranteed',
136-
amount: 5000,
137-
affiliateId: activeAffiliateMock.id,
138-
personId: null,
139-
extCustomerId: '',
140-
extPaymentIntentId: '123456',
141-
extPaymentMethodId: '1234',
142-
billingEmail: '[email protected]',
143-
billingName: 'John Doe',
144-
targetVaultId: vaultMock.id,
145-
chargedAmount: 0,
146-
currency: 'BGN',
147-
createdAt: new Date(),
148-
updatedAt: new Date(),
149-
provider: 'bank',
150-
}
142+
const mockGuaranteedPayment = { ...mockPayment, status: PaymentStatus.guaranteed }
151143

152144
const userMock = {
153145
sub: 'testKeycloackId',
@@ -232,13 +224,13 @@ describe('AffiliateController', () => {
232224

233225
const activeAffiliateMock: Affiliate = {
234226
...affiliateMock,
235-
status: 'active',
227+
status: AffiliateStatus.active,
236228
id: '12345',
237229
affiliateCode: affiliateCodeMock,
238230
}
239231

240232
const mockCancelledStatus: AffiliateStatusUpdateDto = {
241-
newStatus: 'cancelled',
233+
newStatus: AffiliateStatus.cancelled,
242234
}
243235
jest.spyOn(service, 'findOneById').mockResolvedValue(activeAffiliateMock)
244236

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

267259
const updateStatusDto: AffiliateStatusUpdateDto = {
268-
newStatus: 'active',
260+
newStatus: AffiliateStatus.active,
269261
}
270262
const codeGenerationSpy = jest
271263
.spyOn(afCodeGenerator, 'affiliateCodeGenerator')
@@ -305,7 +297,7 @@ describe('AffiliateController', () => {
305297
jest.spyOn(service, 'findOneByCode').mockResolvedValue(activeAffiliateMock)
306298
const createAffiliateDonationSpy = jest
307299
.spyOn(donationService, 'createAffiliateDonation')
308-
.mockResolvedValue(donationResponseMock)
300+
.mockResolvedValue(mockGuaranteedPayment)
309301
jest.spyOn(prismaMock.vault, 'findMany').mockResolvedValue([vaultMock])
310302
prismaMock.campaign.findFirst.mockResolvedValue({
311303
id: '123',
@@ -318,34 +310,34 @@ describe('AffiliateController', () => {
318310
affiliateId: activeAffiliateMock.id,
319311
})
320312
expect(await donationService.createAffiliateDonation(affiliateDonationDto)).toEqual(
321-
donationResponseMock,
313+
mockGuaranteedPayment,
322314
)
323315
})
324316
it('should cancel', async () => {
325-
const cancelledDonationResponse: Donation = {
326-
...donationResponseMock,
327-
status: 'cancelled',
317+
const cancelledDonationResponse: Payments = {
318+
...mockGuaranteedPayment,
319+
status: PaymentStatus.cancelled,
328320
}
329321
jest
330322
.spyOn(donationService, 'getAffiliateDonationById')
331-
.mockResolvedValue(donationResponseMock)
323+
.mockResolvedValue(mockGuaranteedPayment)
332324
jest.spyOn(donationService, 'update').mockResolvedValue(cancelledDonationResponse)
333325
expect(
334-
await controller.cancelAffiliateDonation(affiliateCodeMock, donationResponseMock.id),
326+
await controller.cancelAffiliateDonation(affiliateCodeMock, mockGuaranteedPayment.id),
335327
).toEqual(cancelledDonationResponse)
336328
})
337329
it('should throw error if donation status is succeeded', async () => {
338-
const succeededDonationResponse: Donation = {
339-
...donationResponseMock,
340-
status: 'succeeded',
330+
const succeededDonationResponse: Payments = {
331+
...mockGuaranteedPayment,
332+
status: PaymentStatus.succeeded,
341333
}
342334

343335
jest
344336
.spyOn(donationService, 'getAffiliateDonationById')
345337
.mockResolvedValue(succeededDonationResponse)
346338
const updateDonationStatus = jest.spyOn(donationService, 'update')
347339
expect(
348-
controller.cancelAffiliateDonation(affiliateCodeMock, donationResponseMock.id),
340+
controller.cancelAffiliateDonation(affiliateCodeMock, mockGuaranteedPayment.id),
349341
).rejects.toThrow(new BadRequestException("Donation status can't be updated"))
350342
expect(updateDonationStatus).not.toHaveBeenCalled()
351343
})

apps/api/src/affiliate/affiliate.controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { CreateAffiliateDonationDto } from './dto/create-affiliate-donation.dto'
2323
import { DonationsService } from '../donations/donations.service'
2424
import { shouldAllowStatusChange } from '../donations/helpers/donation-status-updates'
2525
import { affiliateCodeGenerator } from './utils/affiliateCodeGenerator'
26-
import { DonationStatus } from '@prisma/client'
26+
import { PaymentStatus } from '@prisma/client'
2727
import { CampaignService } from '../campaign/campaign.service'
2828
import { RealmViewSupporters, ViewSupporters } from '@podkrepi-bg/podkrepi-types'
2929

@@ -134,7 +134,7 @@ export class AffiliateController {
134134
@Public()
135135
async getAffiliateDonations(
136136
@Param('affiliateCode') affiliateCode: string,
137-
@Query('status') status: DonationStatus | undefined,
137+
@Query('status') status: PaymentStatus | undefined,
138138
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
139139
@Query('limit') limit: number | undefined,
140140
) {
@@ -151,7 +151,7 @@ export class AffiliateController {
151151
async findAffiliateDonationByCustomerId(
152152
@Param('affiliateCode') affiliateCode: string,
153153
@Param('customerId') customerId: string,
154-
@Query('status') status: DonationStatus | undefined,
154+
@Query('status') status: PaymentStatus | undefined,
155155
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
156156
@Query('limit') limit: number | undefined,
157157
) {

apps/api/src/affiliate/affiliate.service.ts

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@nestjs/common'
22
import { PrismaService } from '../prisma/prisma.service'
3-
import { AffiliateStatus, DonationStatus } from '@prisma/client'
3+
import { AffiliateStatus, PaymentStatus } from '@prisma/client'
44

55
@Injectable()
66
export class AffiliateService {
@@ -19,20 +19,24 @@ export class AffiliateService {
1919
async findDonationsByCustomerId(
2020
affiliateCode: string,
2121
extCustomerId: string,
22-
status: DonationStatus | undefined,
22+
status: PaymentStatus | undefined,
2323
currentPage: number,
2424
limit: number | undefined,
2525
) {
2626
return await this.prismaService.affiliate.findFirst({
2727
where: {
2828
affiliateCode,
29-
donations: { some: { extCustomerId: { equals: extCustomerId }, status } },
29+
payments: { some: { extCustomerId, status } },
3030
},
3131
select: {
32-
donations: {
33-
take: limit ? Number(limit) : undefined,
34-
skip: Number((currentPage - 1) * (limit ?? 0)),
35-
include: { metadata: true },
32+
payments: {
33+
select: {
34+
donations: {
35+
take: limit ? Number(limit) : undefined,
36+
skip: Number((currentPage - 1) * (limit ?? 0)),
37+
include: { metadata: true },
38+
},
39+
},
3640
},
3741
},
3842
})
@@ -67,13 +71,22 @@ export class AffiliateService {
6771
async getAffiliateDataByKeycloakId(keycloakId: string) {
6872
return await this.prismaService.affiliate.findFirst({
6973
where: { company: { person: { keycloakId } } },
70-
include: {
71-
donations: {
72-
where: { status: DonationStatus.guaranteed },
74+
select: {
75+
status: true,
76+
affiliateCode: true,
77+
company: { select: { companyName: true } },
78+
payments: {
79+
where: { status: PaymentStatus.guaranteed },
7380
include: {
74-
targetVault: { select: { campaign: { select: { title: true, slug: true } } } },
75-
affiliate: { select: { company: { select: { companyName: true } } } },
76-
metadata: { select: { name: true } },
81+
donations: {
82+
select: {
83+
id: true,
84+
paymentId: true,
85+
targetVault: { select: { campaign: { select: { title: true, slug: true } } } },
86+
metadata: { select: { name: true } },
87+
amount: true,
88+
},
89+
},
7790
},
7891
},
7992
},
@@ -82,19 +95,23 @@ export class AffiliateService {
8295

8396
async findAffiliateDonationsWithPagination(
8497
affiliateCode: string,
85-
status: DonationStatus | undefined,
98+
status: PaymentStatus | undefined,
8699
currentPage: number,
87100
limit: number | undefined,
88101
) {
89102
return await this.prismaService.affiliate.findUnique({
90103
where: { affiliateCode },
91104
select: {
92-
donations: {
93-
orderBy: { createdAt: 'desc' },
94-
where: { status },
95-
take: limit ? Number(limit) : undefined,
96-
skip: Number((currentPage - 1) * (limit ?? 0)),
97-
include: { metadata: true },
105+
payments: {
106+
select: {
107+
donations: {
108+
orderBy: { createdAt: 'desc' },
109+
where: { payment: { status } },
110+
take: limit ? Number(limit) : undefined,
111+
skip: Number((currentPage - 1) * (limit ?? 0)),
112+
include: { metadata: true },
113+
},
114+
},
98115
},
99116
},
100117
})
@@ -104,9 +121,13 @@ export class AffiliateService {
104121
return await this.prismaService.affiliate.findUnique({
105122
where: { affiliateCode },
106123
include: {
107-
donations: {
108-
orderBy: { createdAt: 'desc' },
109-
take: 10,
124+
payments: {
125+
include: {
126+
donations: {
127+
orderBy: { createdAt: 'desc' },
128+
take: 10,
129+
},
130+
},
110131
},
111132
company: { select: { companyName: true, companyNumber: true, legalPersonName: true } },
112133
},

apps/api/src/affiliate/dto/create-affiliate-donation.dto.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { ApiProperty } from '@nestjs/swagger'
2-
import { Currency, DonationStatus, DonationType, PaymentProvider, Prisma } from '@prisma/client'
2+
import {
3+
Currency,
4+
DonationType,
5+
PaymentProvider,
6+
PaymentStatus,
7+
PaymentType,
8+
Prisma,
9+
} from '@prisma/client'
310
import { Expose, Type } from 'class-transformer'
411
import {
512
Equals,
@@ -81,10 +88,10 @@ export class CreateAffiliateDonationDto {
8188
@ValidateNested({ each: true })
8289
metadata: DonationMetadataDto | undefined
8390

84-
public toEntity(targetVaultId: string): Prisma.DonationCreateInput {
91+
public toEntity(targetVaultId: string): Prisma.PaymentCreateInput {
8592
return {
86-
type: DonationType.corporate,
87-
status: DonationStatus.guaranteed,
93+
type: PaymentType.single,
94+
status: PaymentStatus.guaranteed,
8895
provider: PaymentProvider.bank,
8996
currency: this.currency,
9097
amount: this.amount,
@@ -93,16 +100,24 @@ export class CreateAffiliateDonationDto {
93100
extPaymentMethodId: this.extPaymentMethodId ?? '',
94101
billingEmail: this.billingEmail,
95102
billingName: this.billingName,
96-
targetVault: {
97-
connect: {
98-
id: targetVaultId,
103+
donations: {
104+
create: {
105+
type: DonationType.corporate,
106+
amount: this.amount,
107+
108+
person:
109+
this.isAnonymous === false && this.billingEmail
110+
? { connect: { email: this.billingEmail } }
111+
: {},
112+
113+
targetVault: {
114+
connect: {
115+
id: targetVaultId,
116+
},
117+
},
99118
},
100119
},
101120
affiliate: this.affiliateId ? { connect: { id: this.affiliateId } } : {},
102-
person:
103-
this.isAnonymous === false && this.billingEmail
104-
? { connect: { email: this.billingEmail } }
105-
: {},
106121
}
107122
}
108123
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Prisma } from '@prisma/client'
2+
3+
export type AffiliateWithDonation = Prisma.AffiliateGetPayload<{
4+
include: { payments: { include: { donations: true } } }
5+
}>
6+
7+
export type AffiliateWithCompanyPayload = Prisma.AffiliateGetPayload<{
8+
include: { company: { include: { person: true } }; donations: true }
9+
}>

apps/api/src/bank-transactions-file/bank-transactions-file.controller.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { VaultService } from '../vault/vault.service'
2525
import { CampaignService } from '../campaign/campaign.service'
2626
import { DonationsService } from '../donations/donations.service'
2727
import { parseBankTransactionsFile } from './helpers/parser'
28-
import { DonationStatus, DonationType, PaymentProvider } from '@prisma/client'
28+
import { DonationType, PaymentProvider, PaymentStatus, PaymentType } from '@prisma/client'
2929
import { ApiTags } from '@nestjs/swagger'
3030
import {
3131
BankImportResult,
@@ -108,15 +108,32 @@ export class BankTransactionsFileController {
108108

109109
const vault = await this.vaultService.findByCampaignId(campaign.id)
110110
movement.payment.extPaymentMethodId = 'imported bank payment'
111-
movement.payment.targetVaultId = vault[0].id
112-
movement.payment.type = DonationType.donation
113-
movement.payment.status = DonationStatus.succeeded
111+
movement.payment.donations[0].targetVaultId = vault[0].id
112+
movement.payment.type = PaymentType.single
113+
movement.payment.status = PaymentStatus.succeeded
114114
movement.payment.provider = PaymentProvider.bank
115115

116+
const paymentObj: CreateBankPaymentDto = {
117+
provider: PaymentProvider.bank,
118+
status: PaymentStatus.succeeded,
119+
type: PaymentType.single,
120+
extPaymentIntentId: movement.payment.extPaymentIntentId,
121+
extPaymentMethodId: 'imported bank payment',
122+
amount: movement.payment.amount,
123+
currency: movement.payment.currency,
124+
createdAt: movement.payment.createdAt,
125+
extCustomerId: movement.payment.extCustomerId,
126+
donations: {
127+
create: {
128+
type: DonationType.donation,
129+
amount: movement.payment.amount,
130+
targetVault: { connect: { id: vault[0].id } },
131+
},
132+
},
133+
}
134+
116135
try {
117-
bankImportResult.status = await this.donationsService.createUpdateBankPayment(
118-
movement.payment,
119-
)
136+
bankImportResult.status = await this.donationsService.createUpdateBankPayment(paymentObj)
120137
} catch (e) {
121138
const errorMsg = `Error during database import ${movement.paymentRef} : ${e}`
122139
bankImportResult.status = ImportStatus.FAILED

0 commit comments

Comments
 (0)