feat: add comprehensive logging to payment cron for debugging
Every 10 minutes, the cron now logs: - When it triggers and completes - Satoshi rate fetched (or failure) - Number of eligible shareholders found - If 0 found: warns about shareholders with revenue but mismatched criteria - Per-shareholder: revenue, filmmaker ID, lightning address - Payment preparation: amount in sats, destination address - Duplicate detection - Payment send result (completed or failed with error) - Revenue deduction confirmation Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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<Shareholder> = {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ services:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
CACHEBUST: "12"
|
||||
CACHEBUST: "13"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# ── Core ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user