FIX/promotion-rollback-to-credit #9

Merged
Ghasemi merged 3 commits from FIX/promotion-rollback-to-credit into master 2026-09-05 09:54:37 -04:00

3 commits

Author SHA1 Message Date
db2f0afcd5 Merge branch 'FIX/promotion-rollback-to-credit' of https://git.addwin.ir/addwin/promotions into FIX/promotion-rollback-to-credit
# Conflicts:
#	apps/promotions/admin.py
#	apps/promotions/models.py
#	apps/promotions/views_application.py
2026-09-01 15:10:25 +03:30
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
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