## Summary - Mobile access is granted by `POST /api/finance/store/confirm/` after the app finishes a StoreKit or Play Billing purchase. - App Store signed transactions are checked against Apple Root CA G3. Play purchase tokens are checked with the Play Developer API. - The RevenueCat webhook no longer grants a plan. Stripe Checkout and the Stripe webhook stay for the website. - A purchase account token must match the signed-in user, and a transaction already linked to another account is rejected. Access ends when the verified period end passes unless the app confirms again. Client: ai_ml_operations/chat_web_app#122 ## Config - `APPLE_IAP_BUNDLE_ID` (default `ai.hesychia.chat`) - `GOOGLE_PLAY_PACKAGE_NAME` - `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` (JSON or a file path) - Plan `revenuecat_product_id` or `REVENUECAT_PRODUCT_PLAN_MAP` maps the store product to a plan ## Test plan - [x] `python manage.py test monetization.tests.test_store_purchases monetization.tests.test_revenuecat monetization.tests.test_plans_quotas` - [ ] Confirm an App Store sandbox transaction and a Play license-test purchase against a configured backend - [ ] Confirm a token already linked to another user is rejected - [ ] Confirm `GET /subscription/` returns `store_account_token` and flips a lapsed store sub to canceled Reviewed-on: #79
107 lines
3.1 KiB
Python
107 lines
3.1 KiB
Python
from rest_framework import serializers
|
|
|
|
from monetization.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",
|
|
"revenuecat_event_id",
|
|
"revenuecat_store",
|
|
"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",
|
|
"revenuecat_transaction_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 StorePurchaseConfirmSerializer(serializers.Serializer):
|
|
store = serializers.ChoiceField(choices=("app_store", "play_store"))
|
|
product_id = serializers.CharField(max_length=255)
|
|
transaction_id = serializers.CharField(
|
|
max_length=255, required=False, allow_blank=True
|
|
)
|
|
purchase_token = serializers.CharField(
|
|
max_length=4096, required=False, allow_blank=True
|
|
)
|
|
signed_transaction = serializers.CharField(required=False, allow_blank=True)
|
|
|
|
|
|
class SubscriptionPlanSerializer(serializers.ModelSerializer):
|
|
features = serializers.SerializerMethodField()
|
|
store_product_id = serializers.CharField(
|
|
source="revenuecat_product_id", read_only=True
|
|
)
|
|
|
|
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",
|
|
"store_product_id",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def get_features(self, obj):
|
|
return {
|
|
"text_generation": obj.allows_feature("text_generation"),
|
|
"image_generation": obj.allows_feature("image_generation"),
|
|
"rag": obj.allows_feature("rag"),
|
|
"all_future_features": obj.allows_all_future_features,
|
|
}
|