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()
|
@Injectable()
|
||||||
export class PaymentService {
|
export class PaymentService {
|
||||||
|
private readonly logger = new Logger(PaymentService.name);
|
||||||
provider: LightningService;
|
provider: LightningService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -161,10 +162,12 @@ export class PaymentService {
|
|||||||
|
|
||||||
@Cron(CronExpression.EVERY_10_MINUTES)
|
@Cron(CronExpression.EVERY_10_MINUTES)
|
||||||
async handlePaymentsCron() {
|
async handlePaymentsCron() {
|
||||||
|
this.logger.log('[cron] ── Payment cron triggered ──');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.handlePayment('watch'),
|
this.handlePayment('watch'),
|
||||||
this.handlePayment('rent'),
|
this.handlePayment('rent'),
|
||||||
]);
|
]);
|
||||||
|
this.logger.log('[cron] ── Payment cron complete ──');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_10AM)
|
@Cron(CronExpression.EVERY_DAY_AT_10AM)
|
||||||
@@ -195,17 +198,26 @@ export class PaymentService {
|
|||||||
type: 'watch' | 'rent' = 'watch',
|
type: 'watch' | 'rent' = 'watch',
|
||||||
frequency: Frequency = 'automatic',
|
frequency: Frequency = 'automatic',
|
||||||
) {
|
) {
|
||||||
|
this.logger.log(`[payout:${type}] Starting (frequency=${frequency})`);
|
||||||
|
|
||||||
// Rental prices (and therefore rentPendingRevenue) are denominated in
|
// Rental prices (and therefore rentPendingRevenue) are denominated in
|
||||||
// sats, not USD. The satoshi rate is only needed for watch/subscription
|
// sats, not USD. The satoshi rate is only needed for watch/subscription
|
||||||
// payments which store revenue in USD. Skip the rate fetch entirely for
|
// 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.
|
// rent payouts so they are never blocked by rate-API outages or 429s.
|
||||||
let satoshiRate = 0;
|
let satoshiRate = 0;
|
||||||
if (type !== 'rent') {
|
if (type !== 'rent') {
|
||||||
|
try {
|
||||||
satoshiRate = await this.provider.getSatoshiRate();
|
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 column = type === 'watch' ? 'pendingRevenue' : 'rentPendingRevenue';
|
||||||
const minThreshold = type === 'rent' ? 1 : satoshiRate;
|
const minThreshold = type === 'rent' ? 1 : satoshiRate;
|
||||||
|
this.logger.log(`[payout:${type}] Looking for shareholders with ${column} >= ${minThreshold}`);
|
||||||
|
|
||||||
const options: FindManyOptions<Shareholder> = {
|
const options: FindManyOptions<Shareholder> = {
|
||||||
where: {
|
where: {
|
||||||
@@ -226,17 +238,43 @@ export class PaymentService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shareholders = await this.shareholdersRepository.find(options);
|
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) {
|
for (const shareholder of shareholders) {
|
||||||
|
const revenue = type === 'watch' ? shareholder.pendingRevenue : shareholder.rentPendingRevenue;
|
||||||
const selectedPaymentMethod = shareholder.filmmaker.paymentMethods.find(
|
const selectedPaymentMethod = shareholder.filmmaker.paymentMethods.find(
|
||||||
(method) => method.selected,
|
(method) => method.selected,
|
||||||
);
|
);
|
||||||
|
this.logger.log(
|
||||||
|
`[payout:${type}] Shareholder ${shareholder.id}: revenue=${revenue}, ` +
|
||||||
|
`filmmaker=${shareholder.filmmaker?.id}, ` +
|
||||||
|
`lightningAddress=${selectedPaymentMethod?.lightningAddress || 'NONE'}`,
|
||||||
|
);
|
||||||
if (selectedPaymentMethod?.lightningAddress)
|
if (selectedPaymentMethod?.lightningAddress)
|
||||||
await this.sendLightningPaymentToShareholder(
|
await this.sendLightningPaymentToShareholder(
|
||||||
shareholder,
|
shareholder,
|
||||||
satoshiRate,
|
satoshiRate,
|
||||||
type,
|
type,
|
||||||
);
|
);
|
||||||
|
else
|
||||||
|
this.logger.warn(`[payout:${type}] Skipping shareholder ${shareholder.id} — no lightning address`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,6 +305,7 @@ export class PaymentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rounded <= 0) {
|
if (rounded <= 0) {
|
||||||
|
this.logger.log(`[payout:${type}] Shareholder ${shareholder.id}: rounded amount = 0, skipping`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +313,11 @@ export class PaymentService {
|
|||||||
(method) => method.selected && method.type === 'LIGHTNING',
|
(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({
|
const payment = this.paymentsRepository.create({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
shareholderId: shareholder.id,
|
shareholderId: shareholder.id,
|
||||||
@@ -295,12 +339,15 @@ export class PaymentService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existingPayment) {
|
if (existingPayment) {
|
||||||
|
this.logger.warn(`[payout:${type}] Duplicate pending payment detected for shareholder ${shareholder.id}, skipping`);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
await this.paymentsRepository.save(payment);
|
await this.paymentsRepository.save(payment);
|
||||||
|
this.logger.log(`[payout:${type}] Payment record saved: id=${payment.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.logger.log(`[payout:${type}] Sending ${rounded} sats to ${selectedLightningAddress.lightningAddress}...`);
|
||||||
const providerPayment = await this.provider.sendPaymentWithAddress(
|
const providerPayment = await this.provider.sendPaymentWithAddress(
|
||||||
selectedLightningAddress.lightningAddress,
|
selectedLightningAddress.lightningAddress,
|
||||||
payment,
|
payment,
|
||||||
@@ -308,6 +355,7 @@ export class PaymentService {
|
|||||||
|
|
||||||
payment.providerId = providerPayment.id;
|
payment.providerId = providerPayment.id;
|
||||||
payment.status = 'completed';
|
payment.status = 'completed';
|
||||||
|
this.logger.log(`[payout:${type}] Payment COMPLETED: id=${payment.id}, providerId=${providerPayment.id}`);
|
||||||
|
|
||||||
await (type === 'watch'
|
await (type === 'watch'
|
||||||
? this.shareholdersRepository.update(
|
? this.shareholdersRepository.update(
|
||||||
@@ -325,8 +373,10 @@ export class PaymentService {
|
|||||||
updatedAt: () => 'updated_at',
|
updatedAt: () => 'updated_at',
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
} catch {
|
this.logger.log(`[payout:${type}] Revenue deducted from shareholder ${shareholder.id}`);
|
||||||
|
} catch (err) {
|
||||||
payment.status = 'failed';
|
payment.status = 'failed';
|
||||||
|
this.logger.error(`[payout:${type}] Payment FAILED for shareholder ${shareholder.id}: ${err?.message || err}`);
|
||||||
} finally {
|
} finally {
|
||||||
await this.paymentsRepository.save(payment);
|
await this.paymentsRepository.save(payment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
CACHEBUST: "12"
|
CACHEBUST: "13"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# ── Core ─────────────────────────────────────────────
|
# ── Core ─────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user