fix(affiliates): clawbackCommission now refunds seller and returns platform fee#378
fix(affiliates): clawbackCommission now refunds seller and returns platform fee#378Nexu0ps wants to merge 1 commit into
Conversation
…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 SummaryThis PR correctly fixes the core financial bug in
Confidence Score: 2/5Not 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
Sequence DiagramsequenceDiagram
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
|
| 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", | ||
| }); | ||
| } |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
|
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. |
Fixes #354
Problem
clawbackCommission()had a financial bug: on every refund/clawback itcommission_sats(overcharging them by the platform fee they never received), andResult: the seller permanently lost the commission on every refund, and the affiliate was overcharged.
Fix
On clawback of a
paidconversion, the full settlement is now reversed symmetrically:affiliatePayout = commission_sats - platformFee(what they actually received)commission_satsthat was debited at settlementwallet_transactionsentry for auditabilityMirrors the crediting logic in
settleCommissions()exactly, in reverse.