promotions/apps
Ali Asadi 18875af772 FEATURE(promotions): reversible promotion payouts to the advertising transit wallet
When the advertising service discards what a promotion paid for (e.g. a
captured billboard deleted while still pending approval), the promotion
money sitting in the advertising transit wallet needs to go back to the
promotions credit wallet. The promotion itself stays consumed -- only the
money is returned -- and only payouts that landed in the transit wallet
are reversible (a payout straight to the user's wallet is the user's).

- Promotion.rolled_back_at + rollback_to_credit(): row-locked, CAS-stamped,
  idempotent transfer transit -> credit for SUCCESS/transit-destined payouts.
- PromotionRollback model: keyed (user, event_label) -- all the advertising
  side knows. Handles the async race (payout runs in a Celery task, so the
  Promotion may not exist yet): Recipient.promote() checks for an unsettled
  request before paying (suppresses the payout) and after (reverses a
  payout that landed mid-request).
- POST .../event/<event_label>/rollback/ -> {status: reversed|deferred|nothing, amount}.
- migration 0011 (hand-written; verified via makemigrations --dry-run + check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 14:47:05 +03:30
..
gooyal_oauth2 application views 2026-06-26 14:58:36 +03:30
promotions FEATURE(promotions): reversible promotion payouts to the advertising transit wallet 2026-09-01 14:47:05 +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