From 023653eec50c854516b324dbf46bae22407ce0d5 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 14 Feb 2026 16:35:21 +0000 Subject: [PATCH] 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. --- backend/README.md | 12 ++++++ backend/src/webhooks/webhooks.service.ts | 4 +- backend/src/zaps/zaps.service.ts | 51 ++++++++++++++---------- src/services/indeehub-api.service.ts | 12 +++--- 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/backend/README.md b/backend/README.md index f01b3d8..543ab42 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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: — stats recorded for project ` and `Zap stats saved: project 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. +   # Running Stripe Webhooks locally diff --git a/backend/src/webhooks/webhooks.service.ts b/backend/src/webhooks/webhooks.service.ts index 554e348..2bad629 100644 --- a/backend/src/webhooks/webhooks.service.ts +++ b/backend/src/webhooks/webhooks.service.ts @@ -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); } diff --git a/backend/src/zaps/zaps.service.ts b/backend/src/zaps/zaps.service.ts index a818350..3aa6603 100644 --- a/backend/src/zaps/zaps.service.ts +++ b/backend/src/zaps/zaps.service.ts @@ -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 { - 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); } /** diff --git a/src/services/indeehub-api.service.ts b/src/services/indeehub-api.service.ts index 931c2d6..f303297 100644 --- a/src/services/indeehub-api.service.ts +++ b/src/services/indeehub-api.service.ts @@ -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> { 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