Guard manual affiliate payouts by settlement date#213
Conversation
Greptile SummaryThis PR closes the gap where
Confidence Score: 5/5Safe to merge — the guard is minimal, correctly placed, and the new test confirms the rejection path works end-to-end. The change is small and surgical: one additional field in a SELECT and a four-line guard. The guard fires before any side effects (wallet lookups or LN transfers), so it cannot cause double-payment or partial-state issues. The new test validates the rejection response and verifies no downstream mocks are called. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
actor Seller
participant Route as POST /conversions/pay
participant DB as Supabase (service)
participant LN as Lightning (internalTransfer)
Seller->>Route: "POST { conversion_id }"
Route->>DB: SELECT affiliate_offers (verify seller ownership)
DB-->>Route: offer
Route->>DB: SELECT affiliate_conversions (incl. settles_at)
DB-->>Route: conv
alt "status === paid"
Route-->>Seller: 400 Already paid
else "status === clawed_back"
Route-->>Seller: 400 Cannot pay clawed back
else "settles_at set AND settles_at > now"
Route-->>Seller: 400 Conversion has not settled yet
else "commission_sats <= 0"
Route-->>Seller: 400 Commission amount is zero
else all checks pass
Route->>DB: getUserLnWallet (seller)
Route->>DB: getUserLnWallet (affiliate)
Route->>LN: internalTransfer(seller to affiliate)
LN-->>Route: ok
Route->>DB: "UPDATE status=paid, paid_at"
Route->>DB: INSERT wallet_transactions (both parties)
Route-->>Seller: "200 { ok: true, commission_sats }"
end
Reviews (3): Last reviewed commit: "Guard manual affiliate payouts by settle..." | Re-trigger Greptile |
| if (conv.settles_at && new Date(conv.settles_at) > new Date()) { | ||
| return NextResponse.json({ error: "Conversion has not settled yet" }, { status: 400 }); | ||
| } |
There was a problem hiding this comment.
Null
settles_at silently bypasses the guard
The condition conv.settles_at && ... means conversions with no settlement date set are immediately payable via this endpoint. The auto-payout cron uses .lte("settles_at", now), which evaluates to NULL for null rows in Postgres and therefore excludes them — so null records are never auto-paid but can be manually paid here. If null settles_at is intentional design (no configured settlement delay → pay immediately), this is fine and worth a short comment. If null can occur due to a missing value at conversion creation, it creates a gap where the guard is bypassed entirely.
Summary
settles_atRoot cause
The auto-payout cron filters pending conversions with
.lte("settles_at", now), butPOST /api/affiliates/offers/[id]/conversions/paydid not load or checksettles_at. A seller could manually pay a pending commission before the configured settlement delay elapsed.Validation
./node_modules/.bin/vitest run 'src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts'./node_modules/.bin/eslint 'src/app/api/affiliates/offers/[id]/conversions/pay/route.ts' 'src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts'./node_modules/.bin/tsc --noEmit --pretty falsegit diff --check -- 'src/app/api/affiliates/offers/[id]/conversions/pay/route.ts' 'src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts'Fixes #212
Related paid testing gig: https://ugig.net/gigs/4741218f-a723-46bb-82cb-6516120331ae