Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1/2]: Overhaul of donations module #604

Merged
merged 8 commits into from
Mar 10, 2024
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
6 changes: 3 additions & 3 deletions apps/api/src/affiliate/affiliate.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { CreateAffiliateDonationDto } from './dto/create-affiliate-donation.dto'
import { DonationsService } from '../donations/donations.service'
import { shouldAllowStatusChange } from '../donations/helpers/donation-status-updates'
import { affiliateCodeGenerator } from './utils/affiliateCodeGenerator'
import { DonationStatus } from '@prisma/client'
import { PaymentStatus } from '@prisma/client'
import { CampaignService } from '../campaign/campaign.service'
import { RealmViewSupporters, ViewSupporters } from '@podkrepi-bg/podkrepi-types'

Expand Down Expand Up @@ -134,7 +134,7 @@ export class AffiliateController {
@Public()
async getAffiliateDonations(
@Param('affiliateCode') affiliateCode: string,
@Query('status') status: DonationStatus | undefined,
@Query('status') status: PaymentStatus | undefined,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit') limit: number | undefined,
) {
Expand All @@ -151,7 +151,7 @@ export class AffiliateController {
async findAffiliateDonationByCustomerId(
@Param('affiliateCode') affiliateCode: string,
@Param('customerId') customerId: string,
@Query('status') status: DonationStatus | undefined,
@Query('status') status: PaymentStatus | undefined,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit') limit: number | undefined,
) {
Expand Down
67 changes: 44 additions & 23 deletions apps/api/src/affiliate/affiliate.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import { AffiliateStatus, DonationStatus } from '@prisma/client'
import { AffiliateStatus, PaymentStatus } from '@prisma/client'

@Injectable()
export class AffiliateService {
Expand All @@ -19,20 +19,24 @@ export class AffiliateService {
async findDonationsByCustomerId(
affiliateCode: string,
extCustomerId: string,
status: DonationStatus | undefined,
status: PaymentStatus | undefined,
currentPage: number,
limit: number | undefined,
) {
return await this.prismaService.affiliate.findFirst({
where: {
affiliateCode,
donations: { some: { extCustomerId: { equals: extCustomerId }, status } },
payments: { some: { extCustomerId, status } },
},
select: {
donations: {
take: limit ? Number(limit) : undefined,
skip: Number((currentPage - 1) * (limit ?? 0)),
include: { metadata: true },
payments: {
select: {
donations: {
take: limit ? Number(limit) : undefined,
skip: Number((currentPage - 1) * (limit ?? 0)),
include: { metadata: true },
},
},
},
},
})
Expand Down Expand Up @@ -67,13 +71,22 @@ export class AffiliateService {
async getAffiliateDataByKeycloakId(keycloakId: string) {
return await this.prismaService.affiliate.findFirst({
where: { company: { person: { keycloakId } } },
include: {
donations: {
where: { status: DonationStatus.guaranteed },
select: {
status: true,
affiliateCode: true,
company: { select: { companyName: true } },
payments: {
where: { status: PaymentStatus.guaranteed },
include: {
targetVault: { select: { campaign: { select: { title: true, slug: true } } } },
affiliate: { select: { company: { select: { companyName: true } } } },
metadata: { select: { name: true } },
donations: {
select: {
id: true,
paymentId: true,
targetVault: { select: { campaign: { select: { title: true, slug: true } } } },
metadata: { select: { name: true } },
amount: true,
},
},
},
},
},
Expand All @@ -82,19 +95,23 @@ export class AffiliateService {

async findAffiliateDonationsWithPagination(
affiliateCode: string,
status: DonationStatus | undefined,
status: PaymentStatus | undefined,
currentPage: number,
limit: number | undefined,
) {
return await this.prismaService.affiliate.findUnique({
where: { affiliateCode },
select: {
donations: {
orderBy: { createdAt: 'desc' },
where: { status },
take: limit ? Number(limit) : undefined,
skip: Number((currentPage - 1) * (limit ?? 0)),
include: { metadata: true },
payments: {
select: {
donations: {
orderBy: { createdAt: 'desc' },
where: { payment: { status } },
take: limit ? Number(limit) : undefined,
skip: Number((currentPage - 1) * (limit ?? 0)),
include: { metadata: true },
},
},
},
},
})
Expand All @@ -104,9 +121,13 @@ export class AffiliateService {
return await this.prismaService.affiliate.findUnique({
where: { affiliateCode },
include: {
donations: {
orderBy: { createdAt: 'desc' },
take: 10,
payments: {
include: {
donations: {
orderBy: { createdAt: 'desc' },
take: 10,
},
},
},
company: { select: { companyName: true, companyNumber: true, legalPersonName: true } },
},
Expand Down
37 changes: 26 additions & 11 deletions apps/api/src/affiliate/dto/create-affiliate-donation.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { ApiProperty } from '@nestjs/swagger'
import { Currency, DonationStatus, DonationType, PaymentProvider, Prisma } from '@prisma/client'
import {
Currency,
DonationType,
PaymentProvider,
PaymentStatus,
PaymentType,
Prisma,
} from '@prisma/client'
import { Expose, Type } from 'class-transformer'
import {
Equals,
Expand Down Expand Up @@ -81,10 +88,10 @@ export class CreateAffiliateDonationDto {
@ValidateNested({ each: true })
metadata: DonationMetadataDto | undefined

public toEntity(targetVaultId: string): Prisma.DonationCreateInput {
public toEntity(targetVaultId: string): Prisma.PaymentCreateInput {
return {
type: DonationType.corporate,
status: DonationStatus.guaranteed,
type: PaymentType.single,
status: PaymentStatus.guaranteed,
provider: PaymentProvider.bank,
currency: this.currency,
amount: this.amount,
Expand All @@ -93,16 +100,24 @@ export class CreateAffiliateDonationDto {
extPaymentMethodId: this.extPaymentMethodId ?? '',
billingEmail: this.billingEmail,
billingName: this.billingName,
targetVault: {
connect: {
id: targetVaultId,
donations: {
create: {
type: DonationType.corporate,
amount: this.amount,

person:
this.isAnonymous === false && this.billingEmail
? { connect: { email: this.billingEmail } }
: {},

targetVault: {
connect: {
id: targetVaultId,
},
},
},
},
affiliate: this.affiliateId ? { connect: { id: this.affiliateId } } : {},
person:
this.isAnonymous === false && this.billingEmail
? { connect: { email: this.billingEmail } }
: {},
}
}
}
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 @@ -25,7 +25,7 @@ import { VaultService } from '../vault/vault.service'
import { CampaignService } from '../campaign/campaign.service'
import { DonationsService } from '../donations/donations.service'
import { parseBankTransactionsFile } from './helpers/parser'
import { DonationStatus, DonationType, PaymentProvider } from '@prisma/client'
import { DonationType, PaymentProvider, PaymentStatus, PaymentType } from '@prisma/client'
import { ApiTags } from '@nestjs/swagger'
import {
BankImportResult,
Expand Down Expand Up @@ -108,15 +108,32 @@ export class BankTransactionsFileController {

const vault = await this.vaultService.findByCampaignId(campaign.id)
movement.payment.extPaymentMethodId = 'imported bank payment'
movement.payment.targetVaultId = vault[0].id
movement.payment.type = DonationType.donation
movement.payment.status = DonationStatus.succeeded
movement.payment.donations[0].targetVaultId = vault[0].id
movement.payment.type = PaymentType.single
movement.payment.status = PaymentStatus.succeeded
movement.payment.provider = PaymentProvider.bank

const paymentObj: CreateBankPaymentDto = {
provider: PaymentProvider.bank,
status: PaymentStatus.succeeded,
type: PaymentType.single,
extPaymentIntentId: movement.payment.extPaymentIntentId,
extPaymentMethodId: 'imported bank payment',
amount: movement.payment.amount,
currency: movement.payment.currency,
createdAt: movement.payment.createdAt,
extCustomerId: movement.payment.extCustomerId,
donations: {
create: {
type: DonationType.donation,
amount: movement.payment.amount,
targetVault: { connect: { id: vault[0].id } },
},
},
}

try {
bankImportResult.status = await this.donationsService.createUpdateBankPayment(
movement.payment,
)
bankImportResult.status = await this.donationsService.createUpdateBankPayment(paymentObj)
} catch (e) {
const errorMsg = `Error during database import ${movement.paymentRef} : ${e}`
bankImportResult.status = ImportStatus.FAILED
Expand Down
Loading
Loading