Skip to content

Commit

Permalink
Merge branch 'podkrepi-bg:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
quantum-grit authored Sep 26, 2023
2 parents 8759ca2 + 8f3c143 commit 1403b52
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -53,6 +55,8 @@ export class BankTransactionsController {
query?.search,
query?.pageindex,
query?.pagesize,
query?.sortBy,
query?.sortOrder,
)
}

Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/bank-transactions/bank-transactions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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({
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/bank-transactions/dto/bank-transactions-query-dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions apps/api/src/donations/events/stripe-payment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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>(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'
Expand Down
32 changes: 26 additions & 6 deletions apps/api/src/donations/events/stripe-payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
string2RecurringDonationStatus,
getInvoiceData,
getPaymentDataFromCharge,
PaymentData,
} from '../helpers/payment-intent-helpers'
import { DonationStatus, CampaignState } from '@prisma/client'

Expand Down Expand Up @@ -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(
Expand All @@ -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')
Expand Down
74 changes: 74 additions & 0 deletions apps/api/src/donations/events/stripe-payment.testdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 17 additions & 1 deletion apps/api/src/person/person.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,23 @@ 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,
countryCode: true,
cityId: true,
description: true,
organizerRelation: true,
_count: { select: { campaigns: true } },
},
},
},
})
}

async findByEmail(email: string) {
Expand Down
11 changes: 9 additions & 2 deletions apps/api/src/tasks/bank-import/import-transactions.task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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'),
},
],
},
})
Expand All @@ -781,6 +786,7 @@ describe('ImportTransactionsTask', () => {

// 3 < 5 => notify for expiring consent
expect(getConsentLinkSpy).toHaveBeenCalled()

expect(emailService.sendFromTemplate).toHaveBeenCalled()
})
})
Expand All @@ -805,6 +811,7 @@ describe('ImportTransactionsTask', () => {
consents: [
{
iban: IBAN,
status: 'valid',
validUntil: DateTime.now().plus({ days: 6 }).toFormat('yyyy-MM-dd'),
},
],
Expand Down
16 changes: 9 additions & 7 deletions apps/api/src/tasks/bank-import/import-transactions.task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -107,7 +109,7 @@ export class IrisTasks {
const recepient = { to: [this.billingAdminEmail] }
const mail = new ExpiringIrisConsentEmailDto({
daysToExpire,
expiresAt: consent.validUntil,
expiresAt: expDate.toISODate(),
renewLink,
})

Expand Down

0 comments on commit 1403b52

Please sign in to comment.