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:
@@ -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.
|
||||
|
||||
|
||||
|
||||
# Running Stripe Webhooks locally
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user