diff --git a/src/api/challenges/challenges.service.ts b/src/api/challenges/challenges.service.ts index 3112965..93dfc39 100644 --- a/src/api/challenges/challenges.service.ts +++ b/src/api/challenges/challenges.service.ts @@ -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}`); @@ -497,13 +506,28 @@ export class ChallengesService { `Challenge Lock already acquired for ${challenge.id}`, ); } + this.logger.error( + `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( + `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.', ); @@ -511,13 +535,20 @@ export class ChallengesService { throw error; } } finally { - await this.prisma.challenge_lock - .deleteMany({ + try { + const result = await this.prisma.challenge_lock.deleteMany({ 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}`, + ); + } 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, + ); + } } } } diff --git a/src/shared/topcoder/billing-accounts.service.ts b/src/shared/topcoder/billing-accounts.service.ts index b0ca3bd..fdbbbec 100644 --- a/src/shared/topcoder/billing-accounts.service.ts +++ b/src/shared/topcoder/billing-accounts.service.ts @@ -8,12 +8,11 @@ const { TOPCODER_API_V6_BASE_URL, TGBillingAccounts } = ENV_CONFIG; interface LockAmountDTO { challengeId: string; - lockAmount: number; + amount: number; } interface ConsumeAmountDTO { challengeId: string; - consumeAmount: number; - markup?: number; + amount: number; } export interface BAValidation { @@ -40,7 +39,7 @@ export class BillingAccountsService { `${TOPCODER_API_V6_BASE_URL}/billing-accounts/${billingAccountId}/lock-amount`, { method: 'PATCH', - body: JSON.stringify({ param: dto }), + body: JSON.stringify(dto), }, ); } catch (err: any) { @@ -49,7 +48,7 @@ export class BillingAccountsService { 'Failed to lock challenge amount', ); throw new Error( - `Budget Error: Requested amount $${dto.lockAmount} exceeds available budget for Billing Account #${billingAccountId}. + `Budget Error: Requested amount $${dto.amount} exceeds available budget for Billing Account #${billingAccountId}. Please contact the Topcoder Project Manager for further assistance.`, ); } @@ -63,7 +62,7 @@ export class BillingAccountsService { `${TOPCODER_API_V6_BASE_URL}/billing-accounts/${billingAccountId}/consume-amount`, { method: 'PATCH', - body: JSON.stringify({ param: dto }), + body: JSON.stringify(dto), }, ); } catch (err: any) { @@ -111,7 +110,7 @@ export class BillingAccountsService { await this.lockAmount(billingAccountId, { challengeId: baValidation.challengeId!, - lockAmount: + amount: (rollback ? prevAmount : currAmount) * (1 + baValidation.markup!), }); } else if (status === ChallengeStatuses.Completed.toLowerCase()) { @@ -125,9 +124,8 @@ export class BillingAccountsService { if (currAmount !== prevAmount) { await this.consumeAmount(billingAccountId, { challengeId: baValidation.challengeId!, - consumeAmount: + amount: (rollback ? prevAmount : currAmount) * (1 + baValidation.markup!), - markup: baValidation.markup, }); } } else if ( @@ -155,7 +153,7 @@ export class BillingAccountsService { if (currAmount !== prevAmount) { await this.lockAmount(billingAccountId, { challengeId: baValidation.challengeId!, - lockAmount: rollback ? prevAmount : 0, + amount: rollback ? prevAmount : 0, }); } } diff --git a/src/shared/topcoder/topcoder-m2m.service.ts b/src/shared/topcoder/topcoder-m2m.service.ts index d36d1f1..de5f5a2 100644 --- a/src/shared/topcoder/topcoder-m2m.service.ts +++ b/src/shared/topcoder/topcoder-m2m.service.ts @@ -96,6 +96,29 @@ export class TopcoderM2MService { const response = await fetch(url, finalOptions); if (!response.ok) { + let responseBody: unknown; + try { + const text = await response.text(); + try { + responseBody = JSON.parse(text); + } catch { + responseBody = text; + } + } catch (e) { + responseBody = `Failed to read response body: ${e?.message ?? e}`; + } + + this.logger.error( + 'M2M fetch failed', + { + url: String(url), + method: (finalOptions.method ?? 'GET'), + status: response.status, + statusText: response.statusText, + requestBody: (finalOptions as any).body, + responseBody, + }, + ); // Optional: You could throw a custom error here throw new Error(`HTTP error! Status: ${response.status}`); }