From df8b70107eb9ba1a37b8641a5fd218c5b91b98d4 Mon Sep 17 00:00:00 2001 From: quantum-grit <91589884+quantum-grit@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:23:44 +0300 Subject: [PATCH 1/6] added: env variable for CAMPAIGN_ADMIN_MAIL in deployment config (#547) --- manifests/base/deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manifests/base/deployment.yaml b/manifests/base/deployment.yaml index 9707213d3..46b98591e 100644 --- a/manifests/base/deployment.yaml +++ b/manifests/base/deployment.yaml @@ -125,6 +125,8 @@ spec: value: '10' - name: BILLING_ADMIN_MAIL value: billing_admin@podkrepi.bg + - name: CAMPAIGN_ADMIN_MAIL + value: campaign_coordinators@podkrepi.bg - name: IRIS_AGENT_HASH valueFrom: secretKeyRef: From 4bcec53ebe4abf75b55a002bdd9d56a8c4a46033 Mon Sep 17 00:00:00 2001 From: Anton Angelov <123360440+tongo-angelov@users.noreply.github.com> Date: Mon, 18 Sep 2023 11:45:22 +0300 Subject: [PATCH 2/6] return user roles (#551) --- apps/api/src/person/person.service.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/api/src/person/person.service.ts b/apps/api/src/person/person.service.ts index dee1199a8..97573c9de 100644 --- a/apps/api/src/person/person.service.ts +++ b/apps/api/src/person/person.service.ts @@ -91,7 +91,14 @@ export class PersonService { } async findOne(id: string) { - return await this.prisma.person.findFirst({ where: { id } }) + return await this.prisma.person.findFirst({ + where: { id }, + include: { + organizer: { select: { id: true, _count: { select: { campaigns: true } } } }, + coordinators: { select: { id: true, _count: { select: { campaigns: true } } } }, + beneficiaries: { select: { id: true, _count: { select: { campaigns: true } } } }, + }, + }) } async findByEmail(email: string) { From 8fc54d1996d38d8bf0c7de69f731884a2c790a3c Mon Sep 17 00:00:00 2001 From: Nikolay Nachev <44066540+Nnachevvv@users.noreply.github.com> Date: Tue, 19 Sep 2023 10:38:14 +0300 Subject: [PATCH 3/6] Handle payment_intent.payment_failed when card is not accepted (#550) * Handle payment_intent.payment_failed stripewebhook * Add unit tests * Fix test --- .../events/stripe-payment.service.spec.ts | 37 ++++++++++ .../events/stripe-payment.service.ts | 32 ++++++-- .../events/stripe-payment.testdata.ts | 74 +++++++++++++++++++ 3 files changed, 137 insertions(+), 6 deletions(-) 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 48988c2c1..07f4a997b 100644 --- a/apps/api/src/donations/events/stripe-payment.service.spec.ts +++ b/apps/api/src/donations/events/stripe-payment.service.spec.ts @@ -36,6 +36,7 @@ import { mockedCampaignCompeleted, mockedVault, mockChargeEventSucceeded, + mockPaymentEventFailed, } from './stripe-payment.testdata' import { DonationStatus } from '@prisma/client' import { RecurringDonationService } from '../../recurring-donation/recurring-donation.service' @@ -198,6 +199,42 @@ describe('StripePaymentService', () => { }) }) + it('should handle payment_intent.payment_failed', () => { + const payloadString = JSON.stringify(mockPaymentEventFailed, null, 2) + + const header = stripe.webhooks.generateTestHeaderString({ + payload: payloadString, + secret: stripeSecret, + }) + + const campaignService = app.get(CampaignService) + const mockedCampaignById = jest + .spyOn(campaignService, 'getCampaignById') + .mockImplementation(() => Promise.resolve(mockedCampaign)) + + const paymentData = getPaymentData(mockPaymentEventFailed.data.object as Stripe.PaymentIntent) + + const mockedUpdateDonationPayment = jest + .spyOn(campaignService, 'updateDonationPayment') + .mockImplementation(() => Promise.resolve('')) + .mockName('updateDonationPayment') + + return request(app.getHttpServer()) + .post(defaultStripeWebhookEndpoint) + .set('stripe-signature', header) + .type('json') + .send(payloadString) + .expect(201) + .then(() => { + expect(mockedCampaignById).toHaveBeenCalledWith(campaignId) //campaignId from the Stripe Event + expect(mockedUpdateDonationPayment).toHaveBeenCalledWith( + mockedCampaign, + paymentData, + DonationStatus.declined, + ) + }) + }) + it('should handle charge.succeeded for not anonymous user', () => { //Set not anonymous explicitly in the test data ;(mockChargeEventSucceeded.data.object as Stripe.Charge).metadata.isAnonymous = 'false' diff --git a/apps/api/src/donations/events/stripe-payment.service.ts b/apps/api/src/donations/events/stripe-payment.service.ts index 6658ea241..77c8b27ab 100644 --- a/apps/api/src/donations/events/stripe-payment.service.ts +++ b/apps/api/src/donations/events/stripe-payment.service.ts @@ -13,6 +13,7 @@ import { string2RecurringDonationStatus, getInvoiceData, getPaymentDataFromCharge, + PaymentData, } from '../helpers/payment-intent-helpers' import { DonationStatus, CampaignState } from '@prisma/client' @@ -69,6 +70,30 @@ export class StripePaymentService { paymentIntent.metadata as DonationMetadata, ) + const billingData = getPaymentData(paymentIntent) + + this.updatePaymentDonationStatus(paymentIntent, billingData, DonationStatus.cancelled) + } + + @StripeWebhookHandler('payment_intent.payment_failed') + async handlePaymentIntentFailed(event: Stripe.Event) { + const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent + Logger.log( + '[ handlePaymentIntentFailed ]', + paymentIntent, + paymentIntent.metadata as DonationMetadata, + ) + + const billingData = getPaymentData(paymentIntent) + + await this.updatePaymentDonationStatus(paymentIntent, billingData, DonationStatus.declined) + } + + async updatePaymentDonationStatus( + paymentIntent: Stripe.PaymentIntent, + billingData: PaymentData, + donationStatus: DonationStatus, + ) { const metadata: DonationMetadata = paymentIntent.metadata as DonationMetadata if (!metadata.campaignId) { throw new BadRequestException( @@ -79,12 +104,7 @@ export class StripePaymentService { const campaign = await this.campaignService.getCampaignById(metadata.campaignId) - const billingData = getPaymentData(paymentIntent) - await this.campaignService.updateDonationPayment( - campaign, - billingData, - DonationStatus.cancelled, - ) + await this.campaignService.updateDonationPayment(campaign, billingData, donationStatus) } @StripeWebhookHandler('charge.succeeded') diff --git a/apps/api/src/donations/events/stripe-payment.testdata.ts b/apps/api/src/donations/events/stripe-payment.testdata.ts index 0f01525a2..423048da3 100644 --- a/apps/api/src/donations/events/stripe-payment.testdata.ts +++ b/apps/api/src/donations/events/stripe-payment.testdata.ts @@ -206,6 +206,80 @@ export const mockPaymentEventCancelled: Stripe.Event = { type: 'payment_intent.canceled', } +export const mockPaymentEventFailed: Stripe.Event = { + id: 'evt_3LUzB4KApGjVGa9t0lyGsAk8', + object: 'event', + api_version: '2020-08-27', + created: 1660163846, + data: { + object: { + id: 'pi_3LUzB4KApGjVGa9t0NGvE94K', + object: 'payment_intent', + amount: 1065, + amount_capturable: 0, + amount_details: { + tip: {}, + }, + amount_received: 0, + application: null, + application_fee_amount: null, + automatic_payment_methods: null, + canceled_at: null, + cancellation_reason: null, + capture_method: 'automatic', + charges: { + object: 'list', + data: [], + has_more: false, + total_count: 0, + url: '/v1/charges?payment_intent=pi_3LUzB4KApGjVGa9t0NGvE94K', + }, + client_secret: 'pi', + confirmation_method: 'automatic', + created: 1660077446, + currency: 'bgn', + customer: null, + description: null, + invoice: null, + last_payment_error: null, + livemode: false, + metadata: { + campaignId: campaignId, + }, + next_action: null, + on_behalf_of: null, + payment_method: null, + payment_method_options: { + card: { + installments: null, + mandate_options: null, + network: null, + request_three_d_secure: 'automatic', + }, + }, + payment_method_types: ['card'], + processing: null, + receipt_email: null, + review: null, + setup_future_usage: null, + shipping: null, + source: null, + statement_descriptor: null, + statement_descriptor_suffix: null, + status: 'payment_failed', + transfer_data: null, + transfer_group: null, + }, + }, + livemode: false, + pending_webhooks: 1, + request: { + id: null, + idempotency_key: '8a2367c4-45bc-40a0-86c0-cff4c2229bec', + }, + type: 'payment_intent.payment_failed', +} + export const mockChargeEventSucceeded: Stripe.Event = { id: 'evt_3MmYVtKApGjVGa9t1d6zrtYm', object: 'event', From f307b744559e2f0b7b2dd4b86ecf785aef6dbd40 Mon Sep 17 00:00:00 2001 From: Dimitar Filipov Date: Wed, 20 Sep 2023 10:12:34 +0300 Subject: [PATCH 4/6] Add sorting for bank transactions (#549) * add sort option to listBankTransactions * add default sort --- .../bank-transactions/bank-transactions.controller.ts | 4 ++++ .../src/bank-transactions/bank-transactions.service.ts | 10 ++++++++++ .../dto/bank-transactions-query-dto.ts | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/apps/api/src/bank-transactions/bank-transactions.controller.ts b/apps/api/src/bank-transactions/bank-transactions.controller.ts index 127c1e9b1..f4e111217 100644 --- a/apps/api/src/bank-transactions/bank-transactions.controller.ts +++ b/apps/api/src/bank-transactions/bank-transactions.controller.ts @@ -44,6 +44,8 @@ export class BankTransactionsController { @ApiQuery({ name: 'from', required: false, type: Date }) @ApiQuery({ name: 'to', required: false, type: Date }) @ApiQuery({ name: 'search', required: false, type: String }) + @ApiQuery({ name: 'sortBy', required: false, type: String }) + @ApiQuery({ name: 'sortOrder', required: false, type: String }) findAll(@Query() query?: BankTransactionsQueryDto) { return this.bankTransactionsService.listBankTransactions( query?.status, @@ -53,6 +55,8 @@ export class BankTransactionsController { query?.search, query?.pageindex, query?.pagesize, + query?.sortBy, + query?.sortOrder, ) } diff --git a/apps/api/src/bank-transactions/bank-transactions.service.ts b/apps/api/src/bank-transactions/bank-transactions.service.ts index 0fede8086..99c004600 100644 --- a/apps/api/src/bank-transactions/bank-transactions.service.ts +++ b/apps/api/src/bank-transactions/bank-transactions.service.ts @@ -11,6 +11,7 @@ import { } from '@prisma/client' import { ExportService } from '../export/export.service' import { getTemplateByTable } from '../export/helpers/exportableData' +import { Prisma } from '@prisma/client' import { PrismaService } from '../prisma/prisma.service' import { Response } from 'express' import { CreateBankPaymentDto } from '../donations/dto/create-bank-payment.dto' @@ -35,6 +36,8 @@ export class BankTransactionsService { * @param search (Optional) Search by sender info or description * @param pageIndex (Optional) * @param pageSize (Optional) + * @param sortBy (Optional) Sort by a specific field + * @param sortOrder (Optional) Sort order (ascending or descending) */ async listBankTransactions( bankDonationStatus?: BankDonationStatus, @@ -44,7 +47,13 @@ export class BankTransactionsService { search?: string, pageIndex?: number, pageSize?: number, + sortBy?: string, + sortOrder?: string, ) { + const defaultSort: Prisma.BankTransactionOrderByWithRelationInput = { + transactionDate: 'desc', + } + const data = await this.prisma.bankTransaction.findMany({ where: { bankDonationStatus, @@ -65,6 +74,7 @@ export class BankTransactionsService { }, skip: pageIndex && pageSize ? pageIndex * pageSize : undefined, take: pageSize ? pageSize : undefined, + orderBy: [sortBy ? { [sortBy]: sortOrder ? sortOrder : 'desc' } : defaultSort], }) const count = await this.prisma.bankTransaction.count({ diff --git a/apps/api/src/bank-transactions/dto/bank-transactions-query-dto.ts b/apps/api/src/bank-transactions/dto/bank-transactions-query-dto.ts index 943e5687a..fd6d47a0c 100644 --- a/apps/api/src/bank-transactions/dto/bank-transactions-query-dto.ts +++ b/apps/api/src/bank-transactions/dto/bank-transactions-query-dto.ts @@ -43,6 +43,16 @@ export class BankTransactionsQueryDto { @IsOptional() @Transform(({ value }) => toNumber(value)) pagesize?: number + + @Expose() + @IsOptional() + @Transform(({ value }) => falsyToUndefined(value)) + sortBy?: string + + @Expose() + @IsOptional() + @Transform(({ value }) => falsyToUndefined(value)) + sortOrder?: string } function toNumber(value: string, opts: ToNumberOptions = {}): number | undefined { From 4ff31dc9033d0b0650731cbafa3eb64ad5ec7b40 Mon Sep 17 00:00:00 2001 From: Anton Angelov <123360440+tongo-angelov@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:21:46 +0300 Subject: [PATCH 5/6] return full beneficiary info (#552) --- apps/api/src/person/person.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/api/src/person/person.service.ts b/apps/api/src/person/person.service.ts index 97573c9de..3f8c553fa 100644 --- a/apps/api/src/person/person.service.ts +++ b/apps/api/src/person/person.service.ts @@ -96,7 +96,16 @@ export class PersonService { include: { organizer: { select: { id: true, _count: { select: { campaigns: true } } } }, coordinators: { select: { id: true, _count: { select: { campaigns: true } } } }, - beneficiaries: { select: { id: true, _count: { select: { campaigns: true } } } }, + beneficiaries: { + select: { + id: true, + countryCode: true, + cityId: true, + description: true, + organizerRelation: true, + _count: { select: { campaigns: true } }, + }, + }, }, }) } From 8f3c143a50929ab1cb62e6448b8f339d8770413d Mon Sep 17 00:00:00 2001 From: quantum-grit <91589884+quantum-grit@users.noreply.github.com> Date: Sun, 24 Sep 2023 12:43:48 +0300 Subject: [PATCH 6/6] trying to fix wrong consent expiration mail (#553) * added: env variable for CAMPAIGN_ADMIN_MAIL in deployment config * added filtering for valid consents when checking expiration dates --------- Co-authored-by: igoychev --- .../bank-import/import-transactions.task.spec.ts | 11 +++++++++-- .../bank-import/import-transactions.task.ts | 16 +++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts b/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts index cce78f1af..8602e1a59 100644 --- a/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts +++ b/apps/api/src/tasks/bank-import/import-transactions.task.spec.ts @@ -559,8 +559,7 @@ describe('ImportTransactionsTask', () => { it('should handle USD currency and parse the BGN equivalent from the transactionId', () => { const eurTransaction: IrisTransactionInfo = { - transactionId: - 'Booked_6516347588_70001524349032963FTRO23184809601C2023010361.12_20230103', + transactionId: 'Booked_6516347588_70001524349032963FTRO23184809601C2023010361.12_20230103', bookingDate: '2023-01-03', creditorAccount: { iban: 'BG66UNCR70001524349032', @@ -764,8 +763,14 @@ describe('ImportTransactionsTask', () => { consents: [ { iban: IBAN, + status: 'valid', validUntil: DateTime.now().plus({ days: 3 }).toFormat('yyyy-MM-dd'), }, + { + iban: IBAN, + status: 'expired', + validUntil: DateTime.now().minus({ days: 3 }).toFormat('yyyy-MM-dd'), + }, ], }, }) @@ -781,6 +786,7 @@ describe('ImportTransactionsTask', () => { // 3 < 5 => notify for expiring consent expect(getConsentLinkSpy).toHaveBeenCalled() + expect(emailService.sendFromTemplate).toHaveBeenCalled() }) }) @@ -805,6 +811,7 @@ describe('ImportTransactionsTask', () => { consents: [ { iban: IBAN, + status: 'valid', validUntil: DateTime.now().plus({ days: 6 }).toFormat('yyyy-MM-dd'), }, ], diff --git a/apps/api/src/tasks/bank-import/import-transactions.task.ts b/apps/api/src/tasks/bank-import/import-transactions.task.ts index 1d5332517..9ff1ac896 100644 --- a/apps/api/src/tasks/bank-import/import-transactions.task.ts +++ b/apps/api/src/tasks/bank-import/import-transactions.task.ts @@ -90,13 +90,15 @@ export class IrisTasks { }) ).data - // Filter to current IBAN - const consent = consents.consents.find((consent) => consent.iban.trim() === this.IBAN) + // Filter valid consents to the current IBAN + const consent = consents.consents.find( + (consent) => consent.iban.trim() === this.IBAN && consent.status === 'valid', + ) - if (!consent) return - - const expDate = DateTime.fromFormat(consent.validUntil, 'yyyy-MM-dd') - const daysToExpire = Math.ceil(expDate.diff(DateTime.local(), 'days').toObject().days || 0) + const expDate = consent + ? DateTime.fromFormat(consent.validUntil, 'yyyy-MM-dd') + : DateTime.local() //if no valid consent use today to send mail with days to expire 0 + const daysToExpire = Math.ceil(expDate.diff(DateTime.local(), 'days').days || 0) // If less than 5 days till expiration -> notify if (daysToExpire <= this.daysToExpCondition) { @@ -107,7 +109,7 @@ export class IrisTasks { const recepient = { to: [this.billingAdminEmail] } const mail = new ExpiringIrisConsentEmailDto({ daysToExpire, - expiresAt: consent.validUntil, + expiresAt: expDate.toISODate(), renewLink, })