Skip to content

Guard manual affiliate payouts by settlement date#213

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
morganschp:fix-affiliate-pay-settlement-guard
May 23, 2026
Merged

Guard manual affiliate payouts by settlement date#213
ralyodio merged 1 commit into
profullstack:masterfrom
morganschp:fix-affiliate-pay-settlement-guard

Conversation

@morganschp
Copy link
Copy Markdown
Contributor

Summary

  • rejects manual affiliate payout requests when the conversion has not reached settles_at
  • performs the settlement-date guard before wallet lookups or Lightning transfer calls
  • adds route coverage for the unsettled pending-conversion path

Root cause

The auto-payout cron filters pending conversions with .lte("settles_at", now), but POST /api/affiliates/offers/[id]/conversions/pay did not load or check settles_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 false
  • git 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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 23, 2026

Greptile Summary

This PR closes the gap where POST /api/affiliates/offers/[id]/conversions/pay allowed sellers to manually pay affiliate commissions before the configured settlement delay elapsed. The fix adds settles_at to the conversion query and inserts a guard before any wallet or Lightning transfer calls.

  • route.ts: settles_at is now selected from affiliate_conversions, and a 400 guard is returned when the field is set and still in the future — correctly positioned ahead of getUserLnWallet and internalTransfer.
  • route.test.ts: New test file added with a Proxy-based Supabase mock; covers the rejection path and asserts that neither the wallet lookup nor the transfer is invoked when the guard fires.
  • eslint-disable removal: The inline suppression comment above type AnySupabase = any was removed; the PR description confirms ESLint was re-run and passed.

Confidence Score: 5/5

Safe 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

Filename Overview
src/app/api/affiliates/offers/[id]/conversions/pay/route.ts Adds settles_at to the conversion SELECT and a guard that rejects requests before the settlement date; guard is correctly placed before wallet lookups and LN transfer.
src/app/api/affiliates/offers/[id]/conversions/pay/route.test.ts New test file covering the unsettled-conversion rejection path; uses a Proxy-based chainable mock for Supabase and correctly asserts that wallet/transfer mocks are never called.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (3): Last reviewed commit: "Guard manual affiliate payouts by settle..." | Re-trigger Greptile

Comment on lines +64 to +66
if (conv.settles_at && new Date(conv.settles_at) > new Date()) {
return NextResponse.json({ error: "Conversion has not settled yet" }, { status: 400 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@ralyodio ralyodio closed this May 23, 2026
@ralyodio ralyodio reopened this May 23, 2026
@ralyodio ralyodio closed this May 23, 2026
@ralyodio ralyodio reopened this May 23, 2026
@ralyodio ralyodio merged commit 164889f into profullstack:master May 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: manual affiliate payouts can bypass settlement delay

2 participants