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>
116 lines
3.5 KiB
Python
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)
|