diff --git a/backend/src/payment/services/payment.service.ts b/backend/src/payment/services/payment.service.ts index 46c4671..d547460 100644 --- a/backend/src/payment/services/payment.service.ts +++ b/backend/src/payment/services/payment.service.ts @@ -34,6 +34,7 @@ import { SubscriptionType } from 'src/subscriptions/enums/types.enum'; @Injectable() export class PaymentService { + private readonly logger = new Logger(PaymentService.name); provider: LightningService; constructor( @@ -161,10 +162,12 @@ export class PaymentService { @Cron(CronExpression.EVERY_10_MINUTES) async handlePaymentsCron() { + this.logger.log('[cron] ── Payment cron triggered ──'); await Promise.all([ this.handlePayment('watch'), this.handlePayment('rent'), ]); + this.logger.log('[cron] ── Payment cron complete ──'); } @Cron(CronExpression.EVERY_DAY_AT_10AM) @@ -195,17 +198,26 @@ export class PaymentService { type: 'watch' | 'rent' = 'watch', frequency: Frequency = 'automatic', ) { + this.logger.log(`[payout:${type}] Starting (frequency=${frequency})`); + // Rental prices (and therefore rentPendingRevenue) are denominated in // sats, not USD. The satoshi rate is only needed for watch/subscription // payments which store revenue in USD. Skip the rate fetch entirely for // rent payouts so they are never blocked by rate-API outages or 429s. let satoshiRate = 0; if (type !== 'rent') { - satoshiRate = await this.provider.getSatoshiRate(); + try { + satoshiRate = await this.provider.getSatoshiRate(); + this.logger.log(`[payout:${type}] Satoshi rate: ${satoshiRate}`); + } catch (err) { + this.logger.error(`[payout:${type}] Failed to get satoshi rate: ${err.message}`); + return; + } } const column = type === 'watch' ? 'pendingRevenue' : 'rentPendingRevenue'; const minThreshold = type === 'rent' ? 1 : satoshiRate; + this.logger.log(`[payout:${type}] Looking for shareholders with ${column} >= ${minThreshold}`); const options: FindManyOptions = { where: { @@ -226,17 +238,43 @@ export class PaymentService { }; const shareholders = await this.shareholdersRepository.find(options); + this.logger.log(`[payout:${type}] Found ${shareholders.length} eligible shareholder(s)`); + + if (shareholders.length === 0) { + // Extra debug: check if there are shareholders with pending revenue at all + const allWithRevenue = await this.shareholdersRepository + .createQueryBuilder('s') + .select(['s.id', `s.${column === 'pendingRevenue' ? 'pending_revenue' : 'rent_pending_revenue'} as revenue`]) + .where(`s.${column === 'pendingRevenue' ? 'pending_revenue' : 'rent_pending_revenue'} > 0`) + .limit(5) + .getRawMany(); + if (allWithRevenue.length > 0) { + this.logger.warn( + `[payout:${type}] ${allWithRevenue.length} shareholder(s) have revenue > 0 but don't match payout criteria. ` + + `Possible issues: no LIGHTNING payment method, not selected, or wrong withdrawalFrequency. ` + + `Sample: ${JSON.stringify(allWithRevenue)}`, + ); + } + } for (const shareholder of shareholders) { + const revenue = type === 'watch' ? shareholder.pendingRevenue : shareholder.rentPendingRevenue; const selectedPaymentMethod = shareholder.filmmaker.paymentMethods.find( (method) => method.selected, ); + this.logger.log( + `[payout:${type}] Shareholder ${shareholder.id}: revenue=${revenue}, ` + + `filmmaker=${shareholder.filmmaker?.id}, ` + + `lightningAddress=${selectedPaymentMethod?.lightningAddress || 'NONE'}`, + ); if (selectedPaymentMethod?.lightningAddress) await this.sendLightningPaymentToShareholder( shareholder, satoshiRate, type, ); + else + this.logger.warn(`[payout:${type}] Skipping shareholder ${shareholder.id} — no lightning address`); } } @@ -267,6 +305,7 @@ export class PaymentService { } if (rounded <= 0) { + this.logger.log(`[payout:${type}] Shareholder ${shareholder.id}: rounded amount = 0, skipping`); return; } @@ -274,6 +313,11 @@ export class PaymentService { (method) => method.selected && method.type === 'LIGHTNING', ); + this.logger.log( + `[payout:${type}] Preparing payment: shareholder=${shareholder.id}, ` + + `amount=${rounded} sats (${rounded * 1000} msats), address=${selectedLightningAddress?.lightningAddress}`, + ); + const payment = this.paymentsRepository.create({ id: randomUUID(), shareholderId: shareholder.id, @@ -295,12 +339,15 @@ export class PaymentService { }); if (existingPayment) { + this.logger.warn(`[payout:${type}] Duplicate pending payment detected for shareholder ${shareholder.id}, skipping`); return; } else { await this.paymentsRepository.save(payment); + this.logger.log(`[payout:${type}] Payment record saved: id=${payment.id}`); } try { + this.logger.log(`[payout:${type}] Sending ${rounded} sats to ${selectedLightningAddress.lightningAddress}...`); const providerPayment = await this.provider.sendPaymentWithAddress( selectedLightningAddress.lightningAddress, payment, @@ -308,6 +355,7 @@ export class PaymentService { payment.providerId = providerPayment.id; payment.status = 'completed'; + this.logger.log(`[payout:${type}] Payment COMPLETED: id=${payment.id}, providerId=${providerPayment.id}`); await (type === 'watch' ? this.shareholdersRepository.update( @@ -325,8 +373,10 @@ export class PaymentService { updatedAt: () => 'updated_at', }, )); - } catch { + this.logger.log(`[payout:${type}] Revenue deducted from shareholder ${shareholder.id}`); + } catch (err) { payment.status = 'failed'; + this.logger.error(`[payout:${type}] Payment FAILED for shareholder ${shareholder.id}: ${err?.message || err}`); } finally { await this.paymentsRepository.save(payment); } diff --git a/docker-compose.yml b/docker-compose.yml index c41ab8b..6145860 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,7 @@ services: context: ./backend dockerfile: Dockerfile args: - CACHEBUST: "12" + CACHEBUST: "13" restart: unless-stopped environment: # ── Core ─────────────────────────────────────────────