-
Notifications
You must be signed in to change notification settings - Fork 1
[PROD] - Payment Status UI fixes -> master #127
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
Changes from all commits
def6cc2
bc0353a
3f35d39
41706c7
f86bd98
6081202
6cbda6b
1206ee4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,34 +79,58 @@ export class AdminService { | |
|
|
||
| let needsReconciliation = false; | ||
| const winningsId = body.winningsId; | ||
| this.logger.log( | ||
| `updateWinnings called by ${userId} for winningsId=${winningsId}`, | ||
| ); | ||
| this.logger.log(`updateWinnings payload: ${JSON.stringify(body)}`); | ||
|
|
||
| try { | ||
| const payments = await this.getPaymentsByWinningsId( | ||
| winningsId, | ||
| body.paymentId, | ||
| ); | ||
|
|
||
| this.logger.log( | ||
| `Found ${payments.length} payment(s) for winningsId=${winningsId}`, | ||
| ); | ||
| if (payments.length === 0) { | ||
| this.logger.warn( | ||
| `No payments found for winningsId=${winningsId}, paymentId=${body.paymentId}`, | ||
| ); | ||
| throw new NotFoundException('failed to get current payments'); | ||
| } | ||
|
|
||
| let releaseDate; | ||
| if (body.paymentStatus) { | ||
| releaseDate = await this.getPaymentReleaseDateByWinningsId(winningsId); | ||
| this.logger.log( | ||
| `Payment release date for winningsId=${winningsId}: ${releaseDate}`, | ||
| ); | ||
| } | ||
|
|
||
| const transactions: (( | ||
| tx: Prisma.TransactionClient, | ||
| ) => Promise<unknown>)[] = []; | ||
| const now = new Date().getTime(); | ||
|
|
||
| // iterate payments and build transaction list | ||
| payments.forEach((payment) => { | ||
| this.logger.log( | ||
| `Processing payment ${payment.payment_id} (installment ${payment.installment_number}) with current status=${payment.payment_status}`, | ||
| ); | ||
|
|
||
| if ( | ||
| payment.payment_status && | ||
| payment.payment_status === PaymentStatus.CANCELLED | ||
| ) { | ||
| this.logger.warn( | ||
| `Attempt to update cancelled payment ${payment.payment_id} — rejecting`, | ||
| ); | ||
| throw new BadRequestException('cannot update cancelled winnings'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| } | ||
|
|
||
| let version = payment.version ?? 1; | ||
| const queuedActions: string[] = []; | ||
|
|
||
| if (body.description) { | ||
| transactions.push((tx) => | ||
|
|
@@ -129,6 +153,9 @@ export class AdminService { | |
| }, | ||
| }), | ||
| ); | ||
| queuedActions.push( | ||
| `update description -> "${body.description}" (version ${version})`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| ); | ||
|
|
||
| if (payment.installment_number === 1) { | ||
| transactions.push((tx) => | ||
|
|
@@ -140,6 +167,7 @@ export class AdminService { | |
| tx, | ||
| ), | ||
| ); | ||
| queuedActions.push('add audit for description change'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -171,6 +199,9 @@ export class AdminService { | |
| payment.payment_status !== PaymentStatus.ON_HOLD_ADMIN && | ||
| payment.payment_status !== PaymentStatus.PAID | ||
| ) { | ||
| this.logger.warn( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| `Invalid attempt to set OWED for payment ${payment.payment_id} when not on hold admin or paid`, | ||
| ); | ||
| throw new BadRequestException( | ||
| "cannot put a payment back to owed unless it is on hold by an admin, or it's been paid", | ||
| ); | ||
|
|
@@ -180,13 +211,19 @@ export class AdminService { | |
| break; | ||
|
|
||
| default: | ||
| this.logger.warn( | ||
| `Invalid payment status provided: ${body.paymentStatus}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ); | ||
| throw new BadRequestException('invalid payment status provided'); | ||
| } | ||
|
|
||
| if ( | ||
| errMessage && | ||
| payment.payment_status === PaymentStatus.PROCESSING | ||
| ) { | ||
| this.logger.warn( | ||
| `Rejected status change for ${payment.payment_id}: ${errMessage}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ); | ||
| throw new BadRequestException(errMessage); | ||
| } | ||
|
|
||
|
|
@@ -201,11 +238,17 @@ export class AdminService { | |
| tx, | ||
| ), | ||
| ); | ||
| queuedActions.push( | ||
| `update status ${payment.payment_status} -> ${body.paymentStatus}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ); | ||
|
|
||
| paymentStatus = body.paymentStatus as PaymentStatus; | ||
|
|
||
| if (body.paymentStatus === PaymentStatus.OWED) { | ||
| needsReconciliation = true; | ||
| this.logger.log( | ||
| `Payment ${payment.payment_id} marked OWED; will trigger reconciliation later`, | ||
| ); | ||
| } | ||
|
|
||
| if (payment.installment_number === 1) { | ||
|
|
@@ -218,6 +261,7 @@ export class AdminService { | |
| tx, | ||
| ), | ||
| ); | ||
| queuedActions.push('add audit for status change'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -232,6 +276,9 @@ export class AdminService { | |
| PaymentStatus.ON_HOLD_ADMIN, | ||
| ].includes(paymentStatus) | ||
| ) { | ||
| this.logger.warn( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| `Cannot update release date for payment ${payment.payment_id} in status ${paymentStatus}`, | ||
| ); | ||
| throw new BadRequestException( | ||
| `Cannot update release date for payment unless it's in one of the states: ${[ | ||
| PaymentStatus.OWED, | ||
|
|
@@ -251,6 +298,9 @@ export class AdminService { | |
| tx, | ||
| ), | ||
| ); | ||
| queuedActions.push( | ||
| `update release_date ${payment.release_date?.toISOString()} -> ${newReleaseDate.toISOString()}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| ); | ||
|
|
||
| if (payment.installment_number === 1) { | ||
| transactions.push((tx) => | ||
|
|
@@ -262,6 +312,7 @@ export class AdminService { | |
| tx, | ||
| ), | ||
| ); | ||
| queuedActions.push('add audit for release date change'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -297,6 +348,10 @@ export class AdminService { | |
| tx, | ||
| ), | ||
| ); | ||
|
|
||
| queuedActions.push( | ||
| `update amounts -> ${body.paymentAmount.toFixed(2)} (installment 1)`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ); | ||
| } else { | ||
| transactions.push((tx) => | ||
| this.updatePaymentAmount( | ||
|
|
@@ -310,17 +365,35 @@ export class AdminService { | |
| tx, | ||
| ), | ||
| ); | ||
| queuedActions.push( | ||
| `update amounts -> total ${body.paymentAmount.toFixed(2)} (installment ${payment.installment_number})`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| this.logger.log( | ||
| `Queued ${queuedActions.length} action(s) for payment ${payment.payment_id}: ${queuedActions.join( | ||
| ' ; ', | ||
| )}`, | ||
| ); | ||
| }); | ||
|
|
||
| this.logger.log( | ||
| `Executing ${transactions.length} transaction step(s) for winningsId=${winningsId}`, | ||
| ); | ||
|
|
||
| // Run all transaction tasks in a single prisma transaction | ||
| await this.prisma.$transaction(async (tx) => { | ||
| for (const transaction of transactions) { | ||
| await transaction(tx); | ||
| for (let i = 0; i < transactions.length; i++) { | ||
| this.logger.log(`Executing transaction ${i + 1}/${transactions.length}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| await transactions[i](tx); | ||
| } | ||
| }); | ||
|
|
||
| this.logger.log( | ||
| `Successfully executed transactions for winningsId=${winningsId}`, | ||
| ); | ||
|
|
||
| if (needsReconciliation) { | ||
| const winning = await this.prisma.winnings.findFirst({ | ||
| select: { | ||
|
|
@@ -332,16 +405,32 @@ export class AdminService { | |
| }); | ||
|
|
||
| if (winning?.winner_id) { | ||
| this.logger.log( | ||
| `Triggering payments reconciliation for user ${winning.winner_id}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| ); | ||
| await this.paymentsService.reconcileUserPayments(winning.winner_id); | ||
| this.logger.log( | ||
| `Reconciliation triggered for user ${winning.winner_id}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| ); | ||
| } else { | ||
| this.logger.warn( | ||
| `Needs reconciliation but no winner_id found for winningsId=${winningsId}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| ); | ||
| } | ||
| } | ||
|
|
||
| result.data = 'Successfully updated winnings'; | ||
| this.logger.log( | ||
| `updateWinnings completed for winningsId=${winningsId}: ${result.data}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| ); | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof NotFoundException || | ||
| error instanceof BadRequestException | ||
| ) { | ||
| this.logger.warn( | ||
| `updateWinnings validation error for winningsId=${winningsId}: ${error.message}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| ); | ||
| throw error; | ||
| } | ||
| this.logger.error('Updating winnings failed', error); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -470,25 +470,34 @@ export class ChallengesService { | |
|
|
||
| async generateChallengePayments(challengeId: string, userId: string) { | ||
| const challenge = await this.getChallenge(challengeId); | ||
| this.logger.log(`Fetched challenge ${challengeId}`); | ||
|
|
||
| if (!challenge) { | ||
| this.logger.error(`Challenge not found: ${challengeId}`); | ||
| throw new Error('Challenge not found!'); | ||
| } | ||
|
|
||
| this.logger.log(`Challenge ${challenge.id} - "${challenge.name}" with status "${challenge.status}" retrieved`); | ||
|
|
||
| const allowedStatuses = [ | ||
| ChallengeStatuses.Completed.toLowerCase(), | ||
| ChallengeStatuses.CancelledFailedReview.toLowerCase(), | ||
| ]; | ||
|
|
||
| if (!allowedStatuses.includes(challenge.status.toLowerCase())) { | ||
| this.logger.error( | ||
| `Challenge ${challenge.id} isn't in a payable status: ${challenge.status}`, | ||
| ); | ||
| throw new Error("Challenge isn't in a payable status!"); | ||
| } | ||
|
|
||
| // need to read for update (LOCK the rows) | ||
| this.logger.log(`Attempting to acquire lock for challenge ${challenge.id}`); | ||
| try { | ||
| await this.prisma.challenge_lock.create({ | ||
| data: { external_id: challenge.id }, | ||
| }); | ||
| this.logger.log(`Lock acquired for challenge ${challenge.id}`); | ||
| } catch (err: any) { | ||
| if (err.code === 'P2002') { | ||
| this.logger.log(`Challenge Lock already acquired for ${challenge.id}`); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
|
|
@@ -497,27 +506,49 @@ export class ChallengesService { | |
| `Challenge Lock already acquired for ${challenge.id}`, | ||
| ); | ||
| } | ||
| this.logger.error( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| `Failed to acquire lock for challenge ${challenge.id}`, | ||
| err.message ?? err, | ||
| ); | ||
| throw err; | ||
| } | ||
|
|
||
| try { | ||
| this.logger.log(`Starting payment creation for challenge ${challenge.id}`); | ||
| await this.createPayments(challenge, userId); | ||
| this.logger.log(`Payment creation completed for challenge ${challenge.id}`); | ||
| } catch (error) { | ||
| if (error.message.includes('Lock already acquired')) { | ||
| this.logger.error( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| `Error while creating payments for challenge ${challenge.id}`, | ||
| error.message ?? error, | ||
| ); | ||
| if ( | ||
| error && | ||
| (typeof error.message === 'string') && | ||
| error.message.includes('Lock already acquired') | ||
| ) { | ||
| this.logger.log(`Conflict detected while creating payments for ${challenge.id}`); | ||
| throw new ConflictException( | ||
| 'Another payment operation is in progress.', | ||
| ); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } finally { | ||
| await this.prisma.challenge_lock | ||
| .deleteMany({ | ||
| try { | ||
| const result = await this.prisma.challenge_lock.deleteMany({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| where: { external_id: challenge.id }, | ||
| }) | ||
| .catch(() => { | ||
| // swallow errors if lock was already released | ||
| }); | ||
| this.logger.log( | ||
| `Released lock for challenge ${challenge.id}. Rows deleted: ${result.count}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ); | ||
| } catch (releaseErr) { | ||
| // swallow errors if lock was already released but log for observability | ||
| this.logger.error( | ||
| `Failed to release lock for challenge ${challenge.id}`, | ||
| releaseErr.message ?? releaseErr, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[❗❗
security]Logging the entire
bodyobject withJSON.stringifycould potentially expose sensitive information. Consider logging only non-sensitive fields or using a redaction mechanism.