docs: update README and improve zap handling in webhooks and services

- Added a production checklist for Zaps (Lightning) in README.md, detailing necessary steps for historic zap visibility.
- Enhanced comments in webhooks.service.ts to clarify zap handling and webhook requirements for BTCPay Server.
- Improved logging in zaps.service.ts to provide more detailed information on zap payouts and recorded stats.
- Updated error handling in zaps.service.ts to ensure robust logging if zap stats recording fails.
- Refined getZapStats method in indeehub-api.service.ts to clarify mock data usage in development.

These changes improve documentation and enhance the handling of zap-related functionalities across the application.
This commit is contained in:
Dorian
2026-02-14 16:35:21 +00:00
parent 50915f8c52
commit 023653eec5
4 changed files with 52 additions and 27 deletions

View File

@@ -223,6 +223,18 @@ npm run build # after you finish the migrations
npm run typeorm:run-migrations # will apply the migrations to the current DB
```
# Zaps (Lightning) production checklist
For historic zaps to show on film cards and in the movie modal:
1. **Migrations** The `zap_stats` table is created by migration `1762000000000-add-zap-stats`. The Dockerfile runs migrations on startup; if you deploy without Docker, run `npm run typeorm:run-migrations` once.
2. **BTCPay webhook** In BTCPay Server: Store → Webhooks → Add webhook. Set the URL to `https://your-domain/api/webhooks/btcpay` (or `/api/webhooks/btcpay-webhook`). Subscribe to **Invoice settled**. Without this, zap payments are not recorded.
3. **Backend logs** After a zap is paid you should see: `Zap payout completed: <invoiceId> — stats recorded for project <projectId>` and `Zap stats saved: project <id> total N zaps, M sats`. If you see `Failed to record zap stats`, check that the `zap_stats` table exists.
4. **API** `GET /zaps/stats?projectIds=id1,id2` must be reachable (e.g. `https://your-domain/api/zaps/stats?projectIds=...`). It is not cached and does not require auth.
&nbsp;
# Running Stripe Webhooks locally

View File

@@ -92,7 +92,9 @@ export class WebhooksService implements OnModuleInit {
// Not a subscription — continue to season check
}
// Check if it's a zap (user paid us → we pay out to creator)
// Check if it's a zap (user paid us → we pay out to creator).
// Historic zaps are stored in zap_stats and returned by GET /zaps/stats.
// Ensure BTCPay Server has a webhook pointing to: https://your-domain/api/webhooks/btcpay
if (invoice.correlationId?.startsWith('zap:')) {
return await this.zapsService.handleZapPaid(invoiceId, invoice);
}

View File

@@ -109,30 +109,41 @@ export class ZapsService {
`IndeeHub zap — project ${projectId}`,
);
await this.recordZapStats(projectId, sats, zapperPubkey);
this.logger.log(`Zap payout completed: ${invoiceId}`);
this.logger.log(`Zap payout completed: ${invoiceId} — stats recorded for project ${projectId}`);
}
/**
* Record zap in DB for historic display on film cards and modal.
* Never throws: logs errors so webhook still succeeds if DB fails.
*/
private async recordZapStats(projectId: string, sats: number, zapperPubkey?: string): Promise<void> {
let row = await this.zapStatsRepository.findOneBy({ projectId });
if (!row) {
row = this.zapStatsRepository.create({
projectId,
zapCount: 0,
zapAmountSats: 0,
recentZapperPubkeys: [],
});
try {
let row = await this.zapStatsRepository.findOneBy({ projectId });
if (!row) {
row = this.zapStatsRepository.create({
projectId,
zapCount: 0,
zapAmountSats: 0,
recentZapperPubkeys: [],
});
}
row.zapCount += 1;
row.zapAmountSats += sats;
if (
zapperPubkey &&
Array.isArray(row.recentZapperPubkeys) &&
row.recentZapperPubkeys.length < MAX_RECENT_ZAPPERS &&
!row.recentZapperPubkeys.includes(zapperPubkey)
) {
row.recentZapperPubkeys = [...row.recentZapperPubkeys, zapperPubkey];
}
await this.zapStatsRepository.save(row);
this.logger.log(`Zap stats saved: project ${projectId} total ${row.zapCount} zaps, ${row.zapAmountSats} sats`);
} catch (err: any) {
this.logger.error(
`Failed to record zap stats for project ${projectId}: ${err?.message}. Ensure zap_stats table exists (run migrations).`,
);
}
row.zapCount += 1;
row.zapAmountSats += sats;
if (
zapperPubkey &&
Array.isArray(row.recentZapperPubkeys) &&
row.recentZapperPubkeys.length < MAX_RECENT_ZAPPERS &&
!row.recentZapperPubkeys.includes(zapperPubkey)
) {
row.recentZapperPubkeys = [...row.recentZapperPubkeys, zapperPubkey];
}
await this.zapStatsRepository.save(row);
}
/**

View File

@@ -206,8 +206,7 @@ class IndeehubApiService {
/**
* Get zap stats for film cards (count, amount, recent zapper pubkeys) by project id.
* In dev, or when API returns empty (e.g. no zaps in DB yet), merges mock data so the zap UI is visible.
* Set VITE_HIDE_MOCK_ZAPS=true to never show mock zaps in production.
* Mock data only in dev so the UI can be tested; production shows only real backend data.
*/
async getZapStats(projectIds: string[]): Promise<Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }>> {
if (projectIds.length === 0) return {}
@@ -219,12 +218,13 @@ class IndeehubApiService {
{ params: { projectIds: ids } },
)
data = response.data ?? {}
} catch {
} catch (err) {
if (import.meta.env.DEV) {
console.warn('[getZapStats] API failed, using empty stats:', err)
}
data = {}
}
const hideMock = import.meta.env.VITE_HIDE_MOCK_ZAPS === 'true'
const shouldMock = !hideMock && (import.meta.env.DEV || Object.keys(data).length === 0)
if (shouldMock) {
if (import.meta.env.DEV) {
Object.assign(data, this.getMockZapStats(projectIds))
}
return data