promotions/apps/promotions/serializers.py
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

116 lines
3.5 KiB
Python

from rest_framework import serializers
from .models import Promotion, Plan, Event, Recipient
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = (
"label",
"data",
"user",
"application",
)
read_only_fields = ('user', 'application')
class PromotionSerializer(serializers.ModelSerializer):
# base_amount = serializers.IntegerField(required=True)
# promotion_amount = serializers.IntegerField(read_only=True)
event = EventSerializer(read_only=True)
user = serializers.UUIDField(source='user_uuid')
class Meta:
model = Promotion
fields = (
"uuid",
"event",
"promotion_amount",
"state",
"user",
)
class PlanSerializer(serializers.ModelSerializer):
base_amount = serializers.IntegerField(required=True)
promotion_amount = serializers.IntegerField(read_only=True)
class Meta:
model = Plan
fields = ("title",
"description",
"base_amount",
"application",
"promotion_amount"
)
read_only_fields = ('application', "promotion_amount", "title", "description")
# def get_promotion_amount(self, obj):
# user = self.context['request'].user
# return obj.calculate_promotion(user)
class PlanPromotSerializer(serializers.ModelSerializer):
class Meta:
model = Promotion
fields = (
"user",
"application",
)
read_only_fields = ('user', 'application')
class PromoteSerializer(serializers.ModelSerializer):
label = serializers.CharField(write_only=True)
data = serializers.JSONField(write_only=True)
promotions = PromotionSerializer(many=True, read_only=True)
class Meta:
model = Promotion
fields = (
"promotions",
"label",
"data",
)
class UserRecipientSerializer(serializers.ModelSerializer):
class Meta:
model = Recipient
fields = ['label',
"base_amount_field"]
class PromotionStatusSerializer(serializers.Serializer):
event_label = serializers.CharField(read_only=True)
processed = serializers.BooleanField(read_only=True)
promotion_amount = serializers.IntegerField(read_only=True, allow_null=True)
class PromotionRollbackSerializer(serializers.Serializer):
event_label = serializers.CharField(read_only=True)
# reversed -> money moved back out of the advertising transit wallet
# deferred -> payout not settled yet; it will be suppressed when it runs
# nothing -> nothing to reverse (already rolled back, or paid to the user)
status = serializers.ChoiceField(read_only=True, choices=['reversed', 'deferred', 'nothing'])
amount = serializers.IntegerField(read_only=True)
class UserPlanSerializer(serializers.ModelSerializer):
recipients = UserRecipientSerializer(many=True, read_only=True)
class Meta:
model = Plan
fields = ("title",
'banner',
"description",
"description_details",
"recipients",
)
read_only_fields = ("title", "description", "description_details", 'banner', "recipients")
# def get_promotion_amount(self, obj):
# user = self.context['request'].user
# return obj.calculate_promotion(user)