Skip to content

fix(affiliates): clawbackCommission now refunds seller and returns platform fee#378

Closed
Nexu0ps wants to merge 1 commit into
profullstack:masterfrom
Nexu0ps:fix/clawback-commission-refund-354
Closed

fix(affiliates): clawbackCommission now refunds seller and returns platform fee#378
Nexu0ps wants to merge 1 commit into
profullstack:masterfrom
Nexu0ps:fix/clawback-commission-refund-354

Conversation

@Nexu0ps
Copy link
Copy Markdown

@Nexu0ps Nexu0ps commented Jun 2, 2026

Fixes #354

Problem

clawbackCommission() had a financial bug: on every refund/clawback it

  • debited the affiliate by the full commission_sats (overcharging them by the platform fee they never received), and
  • never re-credited the seller or returned the platform fee.

Result: the seller permanently lost the commission on every refund, and the affiliate was overcharged.

Fix

On clawback of a paid conversion, the full settlement is now reversed symmetrically:

  • Affiliate debited by affiliatePayout = commission_sats - platformFee (what they actually received)
  • Seller re-credited the full commission_sats that was debited at settlement
  • Platform fee returned from the platform wallet
  • Each leg records a wallet_transactions entry for auditability

Mirrors the crediting logic in settleCommissions() exactly, in reverse.

…form

Previously clawbackCommission debited the affiliate by the FULL
commission_sats (overcharging them by the platform fee they never
received) and never re-credited the seller or returned the platform fee,
so on every refund the seller permanently lost the commission.

Now on clawback of a paid conversion:
- affiliate is debited by affiliatePayout (commission - platformFee),
  matching what they actually received at settlement
- seller is re-credited the full commission_sats that was debited
- platform fee is returned from the platform wallet
- each leg records a wallet_transactions entry for auditability

Fixes profullstack#354
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR correctly fixes the core financial bug in clawbackCommission: the affiliate is now debited only their net payout (commission_sats - platformFee), the seller is re-credited the full commission_sats, and the platform fee is returned — mirroring settleCommissions in reverse. However, the new multi-step reversal introduces several correctness gaps.

  • No atomic transaction: six-plus sequential DB writes have no rollback path; a mid-flight failure leaves the ledger permanently inconsistent.
  • Phantom platform-fee audit record: the wallet_transactions insert for the platform fee runs unconditionally even when platWallet is null and no wallet mutation occurred.
  • Silent affiliate-wallet miss: if affWallet is null the debit is skipped while seller and platform credits still proceed, conjuring commissionSats from nothing.
  • Math.max(0, …) balance floor: an affiliate who already withdrew their payout is clamped to 0, letting the seller receive full credit against no corresponding debit.

Confidence Score: 2/5

Not safe to merge as-is — the multi-step wallet reversal has no atomic rollback and contains two paths that silently produce incorrect ledger state.

The fix correctly identifies and addresses the original financial bug, but the replacement logic introduces a missing-affiliate-wallet path that credits the seller and platform without debiting the affiliate, a phantom platform-fee audit record when the platform wallet row is absent, and no transactional guarantee across the six-plus DB writes.

src/lib/affiliates/commission.ts — specifically the clawbackCommission function from the paid-status block through the final status update.

Important Files Changed

Filename Overview
src/lib/affiliates/commission.ts Fixes the financial logic in clawbackCommission by reversing each leg of the settlement, but introduces no atomic transaction, a phantom audit record when the platform wallet is absent, a silent money-creation path when the affiliate wallet is missing, and a Math.max(0) floor that allows partial clawbacks to inflate balances.

Sequence Diagram

sequenceDiagram
    participant C as Caller
    participant DB as Database
    participant AW as Affiliate Wallet
    participant SW as Seller Wallet
    participant PW as Platform Wallet

    C->>DB: SELECT conversion with seller_id JOIN
    DB-->>C: conv status commission_sats affiliate_id seller_id
    C->>AW: SELECT balance
    AW-->>C: affWallet
    alt affWallet found
        C->>AW: UPDATE balance minus affiliatePayout
        C->>DB: INSERT wallet_transactions affiliate clawback
    else affWallet missing
        note over C,PW: Debit skipped but seller and platform still credited
    end
    C->>SW: SELECT balance
    SW-->>C: sellerWallet
    C->>SW: UPDATE or INSERT balance plus commissionSats
    C->>DB: INSERT wallet_transactions seller refund
    opt platformFee greater than 0
        C->>PW: SELECT balance
        PW-->>C: platWallet
        alt platWallet found
            C->>PW: UPDATE balance minus platformFee
        else
            note over C,PW: No wallet update but tx record still inserted
        end
        C->>DB: INSERT wallet_transactions platform fee refund
    end
    C->>DB: UPDATE conversion status clawed_back
    note over C,DB: No transaction wrapping partial failures leave ledger inconsistent
Loading

Comments Outside Diff (2)

  1. src/lib/affiliates/commission.ts, line 423-448 (link)

    P1 Phantom audit record when platform wallet is absent

    The wallet_transactions insert is unconditional within the if (platformFee > 0) block — it always runs even when platWallet is null and no wallet update was performed. This creates a phantom audit record claiming platformFee sats were "returned" (balance_after: 0) without any corresponding wallet mutation. The symmetric settleCommissions path avoids this by always inserting a wallet row when platWallet is null. The clawback path should either create the missing wallet (matching settle) or guard the wallet_transactions insert inside the same if (platWallet) block.

  2. src/lib/affiliates/commission.ts, line 360-462 (link)

    P1 No transactional safety across multi-step wallet operations

    The clawback now performs six or more sequential database writes with no shared database transaction or rollback mechanism. If the process crashes between any two operations, the ledger is permanently inconsistent — e.g., the affiliate is debited but the seller is never re-credited, or the seller is re-credited but the conversion is never marked clawed_back so a second clawback can fire again. The entire settlement reversal should run inside a single atomic unit.

Reviews (1): Last reviewed commit: "fix(affiliates): correct clawbackCommiss..." | Re-trigger Greptile

Comment on lines +373 to +390
if (affWallet) {
const newAffBalance = Math.max(0, (affWallet.balance_sats ?? 0) - affiliatePayout);
await (admin as AnySupabase)
.from("wallets")
.update({ balance_sats: newBalance, updated_at: new Date().toISOString() })
.update({ balance_sats: newAffBalance, updated_at: new Date().toISOString() })
.eq("user_id", conv.affiliate_id);

await (admin as AnySupabase)
.from("wallet_transactions")
.insert({
user_id: conv.affiliate_id,
type: "affiliate_commission_clawback",
amount_sats: conv.commission_sats,
balance_after: newBalance,
amount_sats: affiliatePayout,
balance_after: newAffBalance,
reference_id: conversionId,
status: "completed",
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Silent skip on missing affiliate wallet creates money

If affWallet is null the affiliate debit and its transaction record are both silently skipped, but execution continues: the seller is re-credited commissionSats and the platform fee is returned, effectively conjuring commissionSats from nothing. The guard should return an error rather than silently proceeding with partial reversal.

if (wallet) {
const newBalance = Math.max(0, (wallet.balance_sats ?? 0) - conv.commission_sats);
if (affWallet) {
const newAffBalance = Math.max(0, (affWallet.balance_sats ?? 0) - affiliatePayout);
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 Math.max(0, ...) floor allows money creation on clawback

If the affiliate has already withdrawn their payout (balance < affiliatePayout), their balance is silently clamped to 0 instead of reclaiming the full amount. The seller still receives the full commissionSats credit, so the net effect is that commissionSats - affiliateCurrentBalance sats are created from nothing. Consider recording the shortfall or returning an error for under-funded clawbacks.

@Nexu0ps
Copy link
Copy Markdown
Author

Nexu0ps commented Jun 4, 2026

Closing this one. It fixes the original clawback double-charge, but the multi-step reversal it introduces has correctness gaps I'd rather not land: the wallet writes aren't wrapped in a transaction (no rollback if one fails mid-way), and when the affiliate wallet row is missing the seller and platform still get credited without a matching debit. A correct version needs an atomic transaction plus explicit null-wallet handling. Closing rather than merging a partial fix.

@Nexu0ps Nexu0ps closed this Jun 4, 2026
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.

Financial bug: clawbackCommission debits affiliate by full amount but never refunds seller

1 participant