From 54f823d4ff17a49b218e733f874ed9ea84f46023 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 24 Oct 2025 09:38:39 +0300 Subject: [PATCH 1/2] Drop the db transaction when creating winnings --- src/api/winnings/winnings.service.ts | 183 +++++++++++++-------------- 1 file changed, 89 insertions(+), 94 deletions(-) diff --git a/src/api/winnings/winnings.service.ts b/src/api/winnings/winnings.service.ts index b4b969a..e26478c 100644 --- a/src/api/winnings/winnings.service.ts +++ b/src/api/winnings/winnings.service.ts @@ -146,115 +146,110 @@ export class WinningsService { body, ); - return this.prisma.$transaction(async (tx) => { - const originId = await this.originRepo.getOriginIdByName(body.origin, tx); + const originId = await this.originRepo.getOriginIdByName(body.origin); - if (!originId) { - this.logger.warn('Invalid origin provided', { originId }); + if (!originId) { + this.logger.warn('Invalid origin provided', { originId }); - result.error = { - code: HttpStatus.BAD_REQUEST, - message: 'Origin name does not exist', - }; - return result; - } - - const winningModel = { - winner_id: body.winnerId, - type: body.type, - origin_id: originId, - category: body.category, - title: body.title, - description: body.description, - external_id: body.externalId, - attributes: body.attributes, - created_by: userId, - payment: { - create: [] as Partial[], - }, + result.error = { + code: HttpStatus.BAD_REQUEST, + message: 'Origin name does not exist', }; + return result; + } + + const winningModel = { + winner_id: body.winnerId, + type: body.type, + origin_id: originId, + category: body.category, + title: body.title, + description: body.description, + external_id: body.externalId, + attributes: body.attributes, + created_by: userId, + payment: { + create: [] as Partial[], + }, + }; - this.logger.debug('Constructed winning model', { winningModel }); + this.logger.debug('Constructed winning model', { winningModel }); - const payrollPayment = (body.attributes || {})['payroll'] === true; + const payrollPayment = (body.attributes || {})['payroll'] === true; - const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm( + const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm( + body.winnerId, + ); + const hasConnectedPaymentMethod = Boolean( + await this.paymentMethodRepo.getConnectedPaymentMethod(body.winnerId), + ); + const isIdentityVerified = + await this.identityVerificationRepo.completedIdentityVerification( body.winnerId, ); - const hasConnectedPaymentMethod = Boolean( - await this.paymentMethodRepo.getConnectedPaymentMethod(body.winnerId), - ); - const isIdentityVerified = - await this.identityVerificationRepo.completedIdentityVerification( - body.winnerId, - ); - for (const detail of body.details || []) { - const paymentModel = { - gross_amount: Prisma.Decimal(detail.grossAmount), - total_amount: Prisma.Decimal(detail.totalAmount), - installment_number: detail.installmentNumber, - currency: detail.currency, - net_amount: Prisma.Decimal(0), - payment_status: '' as payment_status, - created_by: userId, - billing_account: detail.billingAccount, - challenge_fee: Prisma.Decimal(detail.challengeFee), - }; - - paymentModel.net_amount = Prisma.Decimal(detail.grossAmount); - paymentModel.payment_status = - hasConnectedPaymentMethod && hasActiveTaxForm && isIdentityVerified - ? PaymentStatus.OWED - : PaymentStatus.ON_HOLD; - - if (payrollPayment) { - this.logger.debug( - `Payroll payment detected. Setting payment status to PAID for user ${body.winnerId}`, - ); - paymentModel.payment_status = PaymentStatus.PAID; - await this.setPayrollPaymentMethod(body.winnerId); - } - - winningModel.payment.create.push(paymentModel); - this.logger.debug('Added payment model to winning model', { - paymentModel, - }); + for (const detail of body.details || []) { + const paymentModel = { + gross_amount: Prisma.Decimal(detail.grossAmount), + total_amount: Prisma.Decimal(detail.totalAmount), + installment_number: detail.installmentNumber, + currency: detail.currency, + net_amount: Prisma.Decimal(0), + payment_status: '' as payment_status, + created_by: userId, + billing_account: detail.billingAccount, + challenge_fee: Prisma.Decimal(detail.challengeFee), + }; + + paymentModel.net_amount = Prisma.Decimal(detail.grossAmount); + paymentModel.payment_status = + hasConnectedPaymentMethod && hasActiveTaxForm && isIdentityVerified + ? PaymentStatus.OWED + : PaymentStatus.ON_HOLD; + + if (payrollPayment) { + this.logger.debug( + `Payroll payment detected. Setting payment status to PAID for user ${body.winnerId}`, + ); + paymentModel.payment_status = PaymentStatus.PAID; + await this.setPayrollPaymentMethod(body.winnerId); } - this.logger.debug('Attempting to create winning with nested payments.'); - const createdWinning = await this.prisma.winnings.create({ - data: winningModel as any, + winningModel.payment.create.push(paymentModel); + this.logger.debug('Added payment model to winning model', { + paymentModel, }); + } - if (!createdWinning) { - this.logger.error('Failed to create winning!'); - result.error = { - code: HttpStatus.INTERNAL_SERVER_ERROR, - message: 'Failed to create winning!', - }; - } else { - this.logger.debug('Successfully created winning', { createdWinning }); - } + this.logger.debug('Attempting to create winning with nested payments.'); + const createdWinning = await this.prisma.winnings.create({ + data: winningModel as any, + }); + + if (!createdWinning) { + this.logger.error('Failed to create winning!'); + result.error = { + code: HttpStatus.INTERNAL_SERVER_ERROR, + message: 'Failed to create winning!', + }; + } else { + this.logger.debug('Successfully created winning', { createdWinning }); + } - if ( - !payrollPayment && - (!hasConnectedPaymentMethod || !hasActiveTaxForm) - ) { - const amount = body.details.find( - (d) => d.installmentNumber === 1, - )?.totalAmount; - - if (amount) { - this.logger.debug( - `Sending setup email notification for user ${body.winnerId} with amount ${amount}`, - ); - void this.sendSetupEmailNotification(body.winnerId, amount); - } + if (!payrollPayment && (!hasConnectedPaymentMethod || !hasActiveTaxForm)) { + const amount = body.details.find( + (d) => d.installmentNumber === 1, + )?.totalAmount; + + if (amount) { + this.logger.debug( + `Sending setup email notification for user ${body.winnerId} with amount ${amount}`, + ); + void this.sendSetupEmailNotification(body.winnerId, amount); } + } - this.logger.debug('Transaction completed successfully.'); - return result; - }); + this.logger.debug('Transaction completed successfully.'); + return result; } } From d7d30126ff6c54b8f92b80fd47d7b2c27a8fe708 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 24 Oct 2025 09:46:08 +0300 Subject: [PATCH 2/2] Properly use db transaction context --- .../repository/identity-verification.repo.ts | 22 +- src/api/repository/paymentMethod.repo.ts | 22 +- src/api/repository/taxForm.repo.ts | 9 +- src/api/winnings/winnings.service.ts | 199 ++++++++++-------- 4 files changed, 140 insertions(+), 112 deletions(-) diff --git a/src/api/repository/identity-verification.repo.ts b/src/api/repository/identity-verification.repo.ts index 4c1fd92..6985aa6 100644 --- a/src/api/repository/identity-verification.repo.ts +++ b/src/api/repository/identity-verification.repo.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { verification_status } from '@prisma/client'; +import { Prisma, verification_status } from '@prisma/client'; import { PrismaService } from 'src/shared/global/prisma.service'; @Injectable() @@ -12,14 +12,18 @@ export class IdentityVerificationRepository { * @param userId - The unique identifier of the user. * @returns A promise that resolves to `true` if the user has at least one active identity verification association, otherwise `false`. */ - async completedIdentityVerification(userId: string): Promise { - const count = - await this.prisma.user_identity_verification_associations.count({ - where: { - user_id: userId, - verification_status: verification_status.ACTIVE, - }, - }); + async completedIdentityVerification( + userId: string, + tx?: Prisma.TransactionClient, + ): Promise { + const count = await ( + tx || this.prisma + ).user_identity_verification_associations.count({ + where: { + user_id: userId, + verification_status: verification_status.ACTIVE, + }, + }); return count > 0; } diff --git a/src/api/repository/paymentMethod.repo.ts b/src/api/repository/paymentMethod.repo.ts index bf61af1..92a292f 100644 --- a/src/api/repository/paymentMethod.repo.ts +++ b/src/api/repository/paymentMethod.repo.ts @@ -1,5 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { payment_method_status, user_payment_methods } from '@prisma/client'; +import { + payment_method_status, + Prisma, + user_payment_methods, +} from '@prisma/client'; import { PrismaService } from 'src/shared/global/prisma.service'; @Injectable() @@ -14,14 +18,16 @@ export class PaymentMethodRepository { */ async getConnectedPaymentMethod( userId: string, + tx?: Prisma.TransactionClient, ): Promise { - const connectedUserPaymentMethod = - await this.prisma.user_payment_methods.findFirst({ - where: { - user_id: userId, - status: payment_method_status.CONNECTED, - }, - }); + const connectedUserPaymentMethod = await ( + tx || this.prisma + ).user_payment_methods.findFirst({ + where: { + user_id: userId, + status: payment_method_status.CONNECTED, + }, + }); return connectedUserPaymentMethod; } diff --git a/src/api/repository/taxForm.repo.ts b/src/api/repository/taxForm.repo.ts index 59df1db..fb39884 100644 --- a/src/api/repository/taxForm.repo.ts +++ b/src/api/repository/taxForm.repo.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { tax_form_status } from '@prisma/client'; +import { Prisma, tax_form_status } from '@prisma/client'; import { PrismaService } from 'src/shared/global/prisma.service'; @Injectable() @@ -12,8 +12,11 @@ export class TaxFormRepository { * @param userId user id * @returns true if user has active tax form */ - async hasActiveTaxForm(userId: string): Promise { - const count = await this.prisma.user_tax_form_associations.count({ + async hasActiveTaxForm( + userId: string, + tx?: Prisma.TransactionClient, + ): Promise { + const count = await (tx || this.prisma).user_tax_form_associations.count({ where: { user_id: userId, tax_form_status: tax_form_status.ACTIVE, diff --git a/src/api/winnings/winnings.service.ts b/src/api/winnings/winnings.service.ts index e26478c..32e70e3 100644 --- a/src/api/winnings/winnings.service.ts +++ b/src/api/winnings/winnings.service.ts @@ -88,8 +88,13 @@ export class WinningsService { } } - private async setPayrollPaymentMethod(userId: string) { - const payrollPaymentMethod = await this.prisma.payment_method.findFirst({ + private async setPayrollPaymentMethod( + userId: string, + tx?: Prisma.TransactionClient, + ) { + const payrollPaymentMethod = await ( + tx || this.prisma + ).payment_method.findFirst({ where: { payment_method_type: 'Wipro Payroll', }, @@ -101,7 +106,7 @@ export class WinningsService { } if ( - await this.prisma.user_payment_methods.findFirst({ + await (tx || this.prisma).user_payment_methods.findFirst({ where: { user_id: userId, payment_method_id: payrollPaymentMethod.payment_method_id, @@ -146,110 +151,120 @@ export class WinningsService { body, ); - const originId = await this.originRepo.getOriginIdByName(body.origin); + return this.prisma.$transaction(async (tx) => { + const originId = await this.originRepo.getOriginIdByName(body.origin, tx); - if (!originId) { - this.logger.warn('Invalid origin provided', { originId }); + if (!originId) { + this.logger.warn('Invalid origin provided', { originId }); - result.error = { - code: HttpStatus.BAD_REQUEST, - message: 'Origin name does not exist', - }; - return result; - } + result.error = { + code: HttpStatus.BAD_REQUEST, + message: 'Origin name does not exist', + }; + return result; + } - const winningModel = { - winner_id: body.winnerId, - type: body.type, - origin_id: originId, - category: body.category, - title: body.title, - description: body.description, - external_id: body.externalId, - attributes: body.attributes, - created_by: userId, - payment: { - create: [] as Partial[], - }, - }; + const winningModel = { + winner_id: body.winnerId, + type: body.type, + origin_id: originId, + category: body.category, + title: body.title, + description: body.description, + external_id: body.externalId, + attributes: body.attributes, + created_by: userId, + payment: { + create: [] as Partial[], + }, + }; - this.logger.debug('Constructed winning model', { winningModel }); + this.logger.debug('Constructed winning model', { winningModel }); - const payrollPayment = (body.attributes || {})['payroll'] === true; + const payrollPayment = (body.attributes || {})['payroll'] === true; - const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm( - body.winnerId, - ); - const hasConnectedPaymentMethod = Boolean( - await this.paymentMethodRepo.getConnectedPaymentMethod(body.winnerId), - ); - const isIdentityVerified = - await this.identityVerificationRepo.completedIdentityVerification( + const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm( body.winnerId, + tx, ); - - for (const detail of body.details || []) { - const paymentModel = { - gross_amount: Prisma.Decimal(detail.grossAmount), - total_amount: Prisma.Decimal(detail.totalAmount), - installment_number: detail.installmentNumber, - currency: detail.currency, - net_amount: Prisma.Decimal(0), - payment_status: '' as payment_status, - created_by: userId, - billing_account: detail.billingAccount, - challenge_fee: Prisma.Decimal(detail.challengeFee), - }; - - paymentModel.net_amount = Prisma.Decimal(detail.grossAmount); - paymentModel.payment_status = - hasConnectedPaymentMethod && hasActiveTaxForm && isIdentityVerified - ? PaymentStatus.OWED - : PaymentStatus.ON_HOLD; - - if (payrollPayment) { - this.logger.debug( - `Payroll payment detected. Setting payment status to PAID for user ${body.winnerId}`, + const hasConnectedPaymentMethod = Boolean( + await this.paymentMethodRepo.getConnectedPaymentMethod( + body.winnerId, + tx, + ), + ); + const isIdentityVerified = + await this.identityVerificationRepo.completedIdentityVerification( + body.winnerId, + tx, ); - paymentModel.payment_status = PaymentStatus.PAID; - await this.setPayrollPaymentMethod(body.winnerId); + + for (const detail of body.details || []) { + const paymentModel = { + gross_amount: Prisma.Decimal(detail.grossAmount), + total_amount: Prisma.Decimal(detail.totalAmount), + installment_number: detail.installmentNumber, + currency: detail.currency, + net_amount: Prisma.Decimal(0), + payment_status: '' as payment_status, + created_by: userId, + billing_account: detail.billingAccount, + challenge_fee: Prisma.Decimal(detail.challengeFee), + }; + + paymentModel.net_amount = Prisma.Decimal(detail.grossAmount); + paymentModel.payment_status = + hasConnectedPaymentMethod && hasActiveTaxForm && isIdentityVerified + ? PaymentStatus.OWED + : PaymentStatus.ON_HOLD; + + if (payrollPayment) { + this.logger.debug( + `Payroll payment detected. Setting payment status to PAID for user ${body.winnerId}`, + ); + paymentModel.payment_status = PaymentStatus.PAID; + await this.setPayrollPaymentMethod(body.winnerId, tx); + } + + winningModel.payment.create.push(paymentModel); + this.logger.debug('Added payment model to winning model', { + paymentModel, + }); } - winningModel.payment.create.push(paymentModel); - this.logger.debug('Added payment model to winning model', { - paymentModel, + this.logger.debug('Attempting to create winning with nested payments.'); + const createdWinning = await tx.winnings.create({ + data: winningModel as any, }); - } - - this.logger.debug('Attempting to create winning with nested payments.'); - const createdWinning = await this.prisma.winnings.create({ - data: winningModel as any, - }); - - if (!createdWinning) { - this.logger.error('Failed to create winning!'); - result.error = { - code: HttpStatus.INTERNAL_SERVER_ERROR, - message: 'Failed to create winning!', - }; - } else { - this.logger.debug('Successfully created winning', { createdWinning }); - } - if (!payrollPayment && (!hasConnectedPaymentMethod || !hasActiveTaxForm)) { - const amount = body.details.find( - (d) => d.installmentNumber === 1, - )?.totalAmount; + if (!createdWinning) { + this.logger.error('Failed to create winning!'); + result.error = { + code: HttpStatus.INTERNAL_SERVER_ERROR, + message: 'Failed to create winning!', + }; + } else { + this.logger.debug('Successfully created winning', { createdWinning }); + } - if (amount) { - this.logger.debug( - `Sending setup email notification for user ${body.winnerId} with amount ${amount}`, - ); - void this.sendSetupEmailNotification(body.winnerId, amount); + if ( + !payrollPayment && + (!hasConnectedPaymentMethod || !hasActiveTaxForm) + ) { + const amount = body.details.find( + (d) => d.installmentNumber === 1, + )?.totalAmount; + + if (amount) { + this.logger.debug( + `Sending setup email notification for user ${body.winnerId} with amount ${amount}`, + ); + void this.sendSetupEmailNotification(body.winnerId, amount); + } } - } - this.logger.debug('Transaction completed successfully.'); - return result; + this.logger.debug('Transaction completed successfully.'); + return result; + }); } }