## Summary Implements [#16](#16), [#17](#17), and [#36](#36) in one backend PR. - **#36 Multi-plan catalog**: Founders ($10, public), Standard ($15), Pro ($40), Business ($99), Backer ($0). Future tiers seeded but hidden/`is_selectable=false`. Backer email whitelist auto-assigns Founders-level access with no checkout. - **#36 Feature + prompt gating**: plan feature flags (text vs image); rolling **6h** prompt windows (100 / 200 / 300 / 300 / 300). Enforced in both chat consumers when `ENFORCE_SUBSCRIPTION_GATES=true`. - **#17 Token-period quotas**: optional `monthly_token_quota` on plans + per-user override; calendar-month aggregation from `PromptMetric`; warn/block when reported token totals exceed cap. Null provider usage never fabricated as 0; tracked via `turns_missing_token_usage`. - **#16 Token API exposure**: `tokens_in` / `tokens_out` on conversation + prompt serializers (null when unknown). `GET /api/finance/subscription/` returns plan + usage snapshot for the FE. - Checkout defaults to **Founders**; Stripe paid webhooks assign Founders. Registration/OAuth redeem Backer whitelist and return `needs_checkout`. Companion FE PR: `chat_web_app` branch `feature/plans-quotas-token-usage`. ## Test plan - [ ] `manage.py migrate` seeds five plans; admin can add Backer emails - [ ] Public `GET /api/finance/plans/` returns only Founders - [ ] Register with Backer email → active Backer, `needs_checkout=false`, checkout rejected - [ ] Founders checkout + paid webhook → active Founders subscription - [ ] Chat turn blocked without subscription / when prompt window exceeded / when token period exceeded - [ ] Standard plan denies image feature; Pro/Founders/Backer allow - [ ] Conversation/prompt API returns `null` tokens when unreported, sums when present - [ ] `finance.tests.test_plans_quotas` + existing finance/checkout tests passReviewed-on: #37
87 lines
2.4 KiB
Python
87 lines
2.4 KiB
Python
from rest_framework import serializers
|
|
|
|
from finance.models import Invoice, Payment, SubscriptionPlan
|
|
|
|
|
|
class InvoiceSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Invoice
|
|
fields = [
|
|
"id",
|
|
"provider",
|
|
"status",
|
|
"currency",
|
|
"amount_due",
|
|
"amount_paid",
|
|
"period_start",
|
|
"period_end",
|
|
"stripe_invoice_id",
|
|
"stripe_checkout_session_id",
|
|
"stripe_subscription_id",
|
|
"hosted_invoice_url",
|
|
"description",
|
|
"created",
|
|
"last_modified",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class PaymentSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Payment
|
|
fields = [
|
|
"id",
|
|
"invoice",
|
|
"provider",
|
|
"status",
|
|
"currency",
|
|
"amount",
|
|
"stripe_payment_intent_id",
|
|
"stripe_charge_id",
|
|
"paid_at",
|
|
"failure_message",
|
|
"created",
|
|
"last_modified",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class CheckoutSessionSerializer(serializers.Serializer):
|
|
success_url = serializers.URLField(required=False, allow_blank=False)
|
|
cancel_url = serializers.URLField(required=False, allow_blank=False)
|
|
plan_slug = serializers.SlugField(required=False, allow_blank=False)
|
|
|
|
|
|
class PortalSessionSerializer(serializers.Serializer):
|
|
return_url = serializers.URLField(required=False, allow_blank=False)
|
|
|
|
|
|
class SubscriptionPlanSerializer(serializers.ModelSerializer):
|
|
features = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = SubscriptionPlan
|
|
fields = [
|
|
"slug",
|
|
"name",
|
|
"description",
|
|
"price_cents",
|
|
"currency",
|
|
"interval",
|
|
"is_public",
|
|
"is_selectable",
|
|
"features",
|
|
"prompt_quota_per_window",
|
|
"prompt_window_hours",
|
|
"monthly_token_quota",
|
|
"sort_order",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def get_features(self, obj):
|
|
return {
|
|
"text_generation": obj.allows_feature("text_generation"),
|
|
"image_generation": obj.allows_feature("image_generation"),
|
|
"all_future_features": obj.allows_all_future_features,
|
|
}
|