From c0e7cf4a12344aff9e872f2e66212c2d4b2f0e11 Mon Sep 17 00:00:00 2001 From: Slavcho Ivanov Date: Thu, 25 Jan 2024 17:32:48 +0530 Subject: [PATCH] Invalidate a successfull donation (#597) * Reduce the cache ttl for public donations and total money collected. The idea of the cache is to help in extreme scenarios when many requests are being fired. One request every 2 seconds should be easy to handle by the backend. * The expense original filenames are encoded in base64. This allows us to upload files with cyrilic names. But it adds a bit of complexity in the backend. * Add support for making a donation invalid. Sometimes we can have a buggy stripe donation and we would like to sort of remove it. This is allowed only if one has special credentials, of course. * Refund payment should also be covered by the account-edit-financials-requests role. * Rename the post /invalidate-stripe-payment/:id to a patch /:id/invalidate to match the REST guidelines better. --- .../donations/donations.controller.spec.ts | 24 ++++++++++++++++++ .../api/src/donations/donations.controller.ts | 18 ++++++++++--- apps/api/src/donations/donations.service.ts | 25 +++++++++++++++++++ .../src/lib/roles/team/edit-financials.ts | 3 +++ .../src/lib/roles/team/index.ts | 1 + 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 libs/podkrepi-types/src/lib/roles/team/edit-financials.ts diff --git a/apps/api/src/donations/donations.controller.spec.ts b/apps/api/src/donations/donations.controller.spec.ts index 1ff530713..909625621 100644 --- a/apps/api/src/donations/donations.controller.spec.ts +++ b/apps/api/src/donations/donations.controller.spec.ts @@ -297,4 +297,28 @@ describe('DonationsController', () => { reason: 'requested_by_customer', }) }) + + it('should invalidate a donation and update the vault if needed', async () => { + const existingDonation = { ...mockDonation, status: DonationStatus.succeeded } + jest.spyOn(prismaMock, '$transaction').mockImplementation((callback) => callback(prismaMock)) + + prismaMock.donation.findFirstOrThrow.mockResolvedValueOnce(existingDonation) + + await controller.invalidate('123') + + expect(prismaMock.donation.update).toHaveBeenCalledWith({ + where: { id: '123' }, + data: { + status: DonationStatus.invalid, + }, + }) + expect(prismaMock.vault.update).toHaveBeenCalledWith({ + where: { id: existingDonation.targetVaultId }, + data: { + amount: { + decrement: existingDonation.amount, + }, + }, + }) + }) }) diff --git a/apps/api/src/donations/donations.controller.ts b/apps/api/src/donations/donations.controller.ts index 6ad0c7efd..e7bf46fe2 100644 --- a/apps/api/src/donations/donations.controller.ts +++ b/apps/api/src/donations/donations.controller.ts @@ -16,7 +16,7 @@ import { import { ApiQuery, ApiTags } from '@nestjs/swagger' import { DonationStatus } from '@prisma/client' import { AuthenticatedUser, Public, RoleMatchingMode, Roles } from 'nest-keycloak-connect' -import { RealmViewSupporters, ViewSupporters } from '@podkrepi-bg/podkrepi-types' +import { RealmViewSupporters, ViewSupporters, EditFinancialsRequests } from '@podkrepi-bg/podkrepi-types' import { isAdmin, KeycloakTokenParsed } from '../auth/keycloak' import { DonationsService } from './donations.service' @@ -221,7 +221,7 @@ export class DonationsController { @Post('/refund-stripe-payment/:id') @Roles({ - roles: [RealmViewSupporters.role, ViewSupporters.role], + roles: [EditFinancialsRequests.role], mode: RoleMatchingMode.ANY, }) refundStripePaymet(@Param('id') paymentIntentId: string) { @@ -240,6 +240,16 @@ export class DonationsController { return this.donationsService.createUpdateBankPayment(bankPaymentDto) } + @Patch('/:id/invalidate') + @Roles({ + roles: [EditFinancialsRequests.role], + mode: RoleMatchingMode.ANY, + }) + invalidate(@Param('id') id: string) { + Logger.debug(`Invalidating donation with id ${id}`) + return this.donationsService.invalidate(id) + } + @Patch(':id') @Roles({ roles: [RealmViewSupporters.role, ViewSupporters.role], @@ -251,12 +261,14 @@ export class DonationsController { @Body() updatePaymentDto: UpdatePaymentDto, ) { + Logger.debug(`Updating donation with id ${id}`) + return this.donationsService.update(id, updatePaymentDto) } @Post('delete') @Roles({ - roles: [RealmViewSupporters.role, ViewSupporters.role], + roles: [EditFinancialsRequests.role], mode: RoleMatchingMode.ANY, }) delete( diff --git a/apps/api/src/donations/donations.service.ts b/apps/api/src/donations/donations.service.ts index ab0c3d893..cf8e3eaee 100644 --- a/apps/api/src/donations/donations.service.ts +++ b/apps/api/src/donations/donations.service.ts @@ -712,6 +712,31 @@ export class DonationsService { } } + async invalidate(id: string) { + try { + await this.prisma.$transaction(async (tx) => { + const donation = await this.getDonationById(id) + + if (donation.status === DonationStatus.succeeded) { + await this.vaultService.decrementVaultAmount(donation.targetVaultId, donation.amount, tx) + } + + await this.prisma.donation.update({ + where: { id }, + data: { + status: DonationStatus.invalid, + }, + }) + }) + } catch (err) { + Logger.warn(err.message || err) + const msg = `Invalidation failed. No Donation found with given ID.` + + Logger.warn(msg) + throw new NotFoundException(msg) + } + } + async getDonationsByUser(keycloakId: string, email?: string) { const donations = await this.prisma.donation.findMany({ where: { diff --git a/libs/podkrepi-types/src/lib/roles/team/edit-financials.ts b/libs/podkrepi-types/src/lib/roles/team/edit-financials.ts new file mode 100644 index 000000000..121a0f1ac --- /dev/null +++ b/libs/podkrepi-types/src/lib/roles/team/edit-financials.ts @@ -0,0 +1,3 @@ +export class EditFinancialsRequests { + static readonly role = 'realm:account-edit-financials-requests' +} diff --git a/libs/podkrepi-types/src/lib/roles/team/index.ts b/libs/podkrepi-types/src/lib/roles/team/index.ts index 2fd851eaa..25ed5f954 100644 --- a/libs/podkrepi-types/src/lib/roles/team/index.ts +++ b/libs/podkrepi-types/src/lib/roles/team/index.ts @@ -1,2 +1,3 @@ export * from './view-supporters' export * from './view-contact-requests' +export * from './edit-financials'