promotions/apps
Ali Asadi ea070ad293 FEATURE(promotions): wallet-transaction ledger + reversible payouts
The promotions service had no record of the wallet transfers it makes --
the Promotion row's state was the only trace. Add PromotionTransaction, a
ledger row per transfer (mirrors advertising's AdPayment / escrow's
EscrowWalletPayment): PAYOUT (promotions credit -> recipient wallet) and
ROLLBACK (advertising transit -> promotions credit).

- Promotion.promote() now drives its payout through a PromotionTransaction
  PAYOUT row (.execute() does the submit/verify dance) instead of an
  inline, unrecorded wallet call.
- rollback_promotion_payout(user, event_label): reverses a payout that
  landed in the advertising transit wallet, back to the promotions credit
  wallet, when the advertising side discards what it paid for (e.g. a
  captured billboard deleted while pending approval). The Promotion stays
  consumed -- only the money moves; payouts straight to the user's wallet
  are not reversible. Idempotent; returns reversed | deferred | nothing.
- The ROLLBACK row doubles as the async-race marker: when the request
  arrives before the Celery payout task has run, a ROLLBACK row is
  recorded and Recipient.promote() suppresses (or, if it raced, reverses)
  the payout. Replaces the separate PromotionRollback table from the first
  cut of this change.
- POST .../application/<user>/event/<event_label>/rollback/
- migration 0011 (hand-written; verified via makemigrations --dry-run + check)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 15:07:37 +03:30
..
gooyal_oauth2 application views 2026-06-26 14:58:36 +03:30
promotions FEATURE(promotions): wallet-transaction ledger + reversible payouts 2026-09-01 15:07:37 +03:30
users init 2025-07-22 16:05:03 +03:30
__init__.py basic promotion handler 2025-08-03 13:28:40 +03:30