django-cfg 1.2.21__py3-none-any.whl → 1.2.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/newsletter/signals.py +9 -8
- django_cfg/apps/payments/__init__.py +8 -0
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/apps.py +22 -0
- django_cfg/apps/payments/config/__init__.py +87 -0
- django_cfg/apps/payments/config/module.py +162 -0
- django_cfg/apps/payments/config/providers.py +93 -0
- django_cfg/apps/payments/config/settings.py +136 -0
- django_cfg/apps/payments/config/utils.py +198 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/managers/__init__.py +22 -0
- django_cfg/apps/payments/managers/api_key_manager.py +35 -0
- django_cfg/apps/payments/managers/balance_manager.py +361 -0
- django_cfg/apps/payments/managers/currency_manager.py +32 -0
- django_cfg/apps/payments/managers/payment_manager.py +44 -0
- django_cfg/apps/payments/managers/subscription_manager.py +37 -0
- django_cfg/apps/payments/managers/tariff_manager.py +29 -0
- django_cfg/apps/payments/middleware/__init__.py +13 -0
- django_cfg/apps/payments/middleware/api_access.py +261 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +1003 -0
- django_cfg/apps/payments/migrations/__init__.py +1 -0
- django_cfg/apps/payments/models/__init__.py +67 -0
- django_cfg/apps/payments/models/api_keys.py +96 -0
- django_cfg/apps/payments/models/balance.py +209 -0
- django_cfg/apps/payments/models/base.py +30 -0
- django_cfg/apps/payments/models/currencies.py +138 -0
- django_cfg/apps/payments/models/events.py +73 -0
- django_cfg/apps/payments/models/payments.py +301 -0
- django_cfg/apps/payments/models/subscriptions.py +270 -0
- django_cfg/apps/payments/models/tariffs.py +102 -0
- django_cfg/apps/payments/serializers/__init__.py +56 -0
- django_cfg/apps/payments/serializers/api_keys.py +51 -0
- django_cfg/apps/payments/serializers/balance.py +59 -0
- django_cfg/apps/payments/serializers/currencies.py +55 -0
- django_cfg/apps/payments/serializers/payments.py +62 -0
- django_cfg/apps/payments/serializers/subscriptions.py +71 -0
- django_cfg/apps/payments/serializers/tariffs.py +56 -0
- django_cfg/apps/payments/services/__init__.py +65 -0
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +449 -0
- django_cfg/apps/payments/services/core/payment_service.py +393 -0
- django_cfg/apps/payments/services/core/subscription_service.py +616 -0
- django_cfg/apps/payments/services/internal_types.py +266 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/providers/__init__.py +19 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +99 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +150 -0
- django_cfg/apps/payments/signals/payment_signals.py +127 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/urls.py +78 -0
- django_cfg/apps/payments/utils/__init__.py +42 -0
- django_cfg/apps/payments/utils/config_utils.py +243 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/__init__.py +62 -0
- django_cfg/apps/payments/views/api_key_views.py +164 -0
- django_cfg/apps/payments/views/balance_views.py +75 -0
- django_cfg/apps/payments/views/currency_views.py +111 -0
- django_cfg/apps/payments/views/payment_views.py +111 -0
- django_cfg/apps/payments/views/subscription_views.py +135 -0
- django_cfg/apps/payments/views/tariff_views.py +131 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/core/config.py +6 -0
- django_cfg/models/revolution.py +14 -0
- django_cfg/modules/base.py +9 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
"""
|
2
|
+
DRF serializers for the universal payments system.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .balance import (
|
6
|
+
UserBalanceSerializer, TransactionSerializer, TransactionListSerializer
|
7
|
+
)
|
8
|
+
from .payments import (
|
9
|
+
UniversalPaymentSerializer, PaymentCreateSerializer, PaymentListSerializer
|
10
|
+
)
|
11
|
+
from .subscriptions import (
|
12
|
+
SubscriptionSerializer, SubscriptionCreateSerializer, SubscriptionListSerializer,
|
13
|
+
EndpointGroupSerializer
|
14
|
+
)
|
15
|
+
from .api_keys import (
|
16
|
+
APIKeySerializer, APIKeyCreateSerializer, APIKeyListSerializer
|
17
|
+
)
|
18
|
+
from .currencies import (
|
19
|
+
CurrencySerializer, CurrencyNetworkSerializer, CurrencyListSerializer
|
20
|
+
)
|
21
|
+
from .tariffs import (
|
22
|
+
TariffSerializer, TariffEndpointGroupSerializer, TariffListSerializer
|
23
|
+
)
|
24
|
+
|
25
|
+
__all__ = [
|
26
|
+
# Balance
|
27
|
+
'UserBalanceSerializer',
|
28
|
+
'TransactionSerializer',
|
29
|
+
'TransactionListSerializer',
|
30
|
+
|
31
|
+
# Payments
|
32
|
+
'UniversalPaymentSerializer',
|
33
|
+
'PaymentCreateSerializer',
|
34
|
+
'PaymentListSerializer',
|
35
|
+
|
36
|
+
# Subscriptions
|
37
|
+
'SubscriptionSerializer',
|
38
|
+
'SubscriptionCreateSerializer',
|
39
|
+
'SubscriptionListSerializer',
|
40
|
+
'EndpointGroupSerializer',
|
41
|
+
|
42
|
+
# API Keys
|
43
|
+
'APIKeySerializer',
|
44
|
+
'APIKeyCreateSerializer',
|
45
|
+
'APIKeyListSerializer',
|
46
|
+
|
47
|
+
# Currencies
|
48
|
+
'CurrencySerializer',
|
49
|
+
'CurrencyNetworkSerializer',
|
50
|
+
'CurrencyListSerializer',
|
51
|
+
|
52
|
+
# Tariffs
|
53
|
+
'TariffSerializer',
|
54
|
+
'TariffEndpointGroupSerializer',
|
55
|
+
'TariffListSerializer',
|
56
|
+
]
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""
|
2
|
+
API key serializers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rest_framework import serializers
|
6
|
+
from ..models import APIKey
|
7
|
+
|
8
|
+
|
9
|
+
class APIKeySerializer(serializers.ModelSerializer):
|
10
|
+
"""API key with usage stats."""
|
11
|
+
|
12
|
+
is_valid = serializers.SerializerMethodField()
|
13
|
+
|
14
|
+
class Meta:
|
15
|
+
model = APIKey
|
16
|
+
fields = [
|
17
|
+
'id', 'name', 'key_value', 'key_prefix', 'usage_count',
|
18
|
+
'is_active', 'is_valid', 'last_used', 'expires_at',
|
19
|
+
'created_at'
|
20
|
+
]
|
21
|
+
read_only_fields = ['key_value', 'key_prefix', 'usage_count', 'last_used', 'created_at']
|
22
|
+
|
23
|
+
def get_is_valid(self, obj):
|
24
|
+
"""Get validation status."""
|
25
|
+
return obj.is_valid()
|
26
|
+
|
27
|
+
|
28
|
+
class APIKeyCreateSerializer(serializers.ModelSerializer):
|
29
|
+
"""Create API key."""
|
30
|
+
|
31
|
+
class Meta:
|
32
|
+
model = APIKey
|
33
|
+
fields = ['name', 'expires_at']
|
34
|
+
|
35
|
+
|
36
|
+
class APIKeyListSerializer(serializers.ModelSerializer):
|
37
|
+
"""Simplified API key for lists."""
|
38
|
+
|
39
|
+
is_valid = serializers.SerializerMethodField()
|
40
|
+
|
41
|
+
class Meta:
|
42
|
+
model = APIKey
|
43
|
+
fields = [
|
44
|
+
'id', 'name', 'key_prefix', 'usage_count', 'is_active', 'is_valid',
|
45
|
+
'last_used', 'expires_at', 'created_at'
|
46
|
+
]
|
47
|
+
read_only_fields = ['key_prefix', 'usage_count', 'last_used', 'created_at']
|
48
|
+
|
49
|
+
def get_is_valid(self, obj):
|
50
|
+
"""Get validation status."""
|
51
|
+
return obj.is_valid()
|
@@ -0,0 +1,59 @@
|
|
1
|
+
"""
|
2
|
+
Balance serializers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rest_framework import serializers
|
6
|
+
from ..models import UserBalance, Transaction
|
7
|
+
|
8
|
+
|
9
|
+
class UserBalanceSerializer(serializers.ModelSerializer):
|
10
|
+
"""User balance with computed fields."""
|
11
|
+
|
12
|
+
total_balance = serializers.ReadOnlyField()
|
13
|
+
pending_payments_count = serializers.SerializerMethodField()
|
14
|
+
|
15
|
+
class Meta:
|
16
|
+
model = UserBalance
|
17
|
+
fields = [
|
18
|
+
'amount_usd', 'reserved_usd', 'total_balance',
|
19
|
+
'total_earned', 'total_spent', 'last_transaction_at',
|
20
|
+
'pending_payments_count', 'created_at', 'updated_at'
|
21
|
+
]
|
22
|
+
read_only_fields = [
|
23
|
+
'total_earned', 'total_spent', 'last_transaction_at',
|
24
|
+
'created_at', 'updated_at'
|
25
|
+
]
|
26
|
+
|
27
|
+
def get_pending_payments_count(self, obj):
|
28
|
+
"""Get count of pending payments."""
|
29
|
+
return obj.user.universal_payments.filter(status='pending').count()
|
30
|
+
|
31
|
+
|
32
|
+
class TransactionSerializer(serializers.ModelSerializer):
|
33
|
+
"""Transaction with details."""
|
34
|
+
|
35
|
+
transaction_type_display = serializers.CharField(source='get_transaction_type_display', read_only=True)
|
36
|
+
is_credit = serializers.ReadOnlyField()
|
37
|
+
is_debit = serializers.ReadOnlyField()
|
38
|
+
|
39
|
+
class Meta:
|
40
|
+
model = Transaction
|
41
|
+
fields = [
|
42
|
+
'id', 'amount_usd', 'transaction_type', 'transaction_type_display',
|
43
|
+
'description', 'balance_before', 'balance_after',
|
44
|
+
'is_credit', 'is_debit', 'reference_id', 'created_at'
|
45
|
+
]
|
46
|
+
read_only_fields = ['id', 'balance_before', 'balance_after', 'created_at']
|
47
|
+
|
48
|
+
|
49
|
+
class TransactionListSerializer(serializers.ModelSerializer):
|
50
|
+
"""Simplified transaction for lists."""
|
51
|
+
|
52
|
+
transaction_type_display = serializers.CharField(source='get_transaction_type_display', read_only=True)
|
53
|
+
|
54
|
+
class Meta:
|
55
|
+
model = Transaction
|
56
|
+
fields = [
|
57
|
+
'id', 'amount_usd', 'transaction_type', 'transaction_type_display',
|
58
|
+
'description', 'balance_after', 'created_at'
|
59
|
+
]
|
@@ -0,0 +1,55 @@
|
|
1
|
+
"""
|
2
|
+
Currency serializers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rest_framework import serializers
|
6
|
+
from ..models import Currency, CurrencyNetwork
|
7
|
+
|
8
|
+
|
9
|
+
class CurrencySerializer(serializers.ModelSerializer):
|
10
|
+
"""Currency with type info."""
|
11
|
+
|
12
|
+
currency_type_display = serializers.CharField(source='get_currency_type_display', read_only=True)
|
13
|
+
is_crypto = serializers.SerializerMethodField()
|
14
|
+
is_fiat = serializers.SerializerMethodField()
|
15
|
+
|
16
|
+
class Meta:
|
17
|
+
model = Currency
|
18
|
+
fields = [
|
19
|
+
'id', 'code', 'name', 'symbol', 'currency_type', 'currency_type_display',
|
20
|
+
'is_crypto', 'is_fiat', 'decimal_places', 'usd_rate', 'rate_updated_at',
|
21
|
+
'is_active', 'min_payment_amount'
|
22
|
+
]
|
23
|
+
read_only_fields = ['rate_updated_at']
|
24
|
+
|
25
|
+
def get_is_crypto(self, obj):
|
26
|
+
"""Check if currency is crypto."""
|
27
|
+
return obj.is_crypto
|
28
|
+
|
29
|
+
def get_is_fiat(self, obj):
|
30
|
+
"""Check if currency is fiat."""
|
31
|
+
return obj.is_fiat
|
32
|
+
|
33
|
+
|
34
|
+
class CurrencyNetworkSerializer(serializers.ModelSerializer):
|
35
|
+
"""Currency network with status."""
|
36
|
+
|
37
|
+
currency_code = serializers.CharField(source='currency.code', read_only=True)
|
38
|
+
currency_name = serializers.CharField(source='currency.name', read_only=True)
|
39
|
+
|
40
|
+
class Meta:
|
41
|
+
model = CurrencyNetwork
|
42
|
+
fields = [
|
43
|
+
'id', 'currency', 'currency_code', 'currency_name', 'network_code',
|
44
|
+
'network_name', 'is_active', 'confirmation_blocks'
|
45
|
+
]
|
46
|
+
|
47
|
+
|
48
|
+
class CurrencyListSerializer(serializers.ModelSerializer):
|
49
|
+
"""Simplified currency for lists."""
|
50
|
+
|
51
|
+
currency_type_display = serializers.CharField(source='get_currency_type_display', read_only=True)
|
52
|
+
|
53
|
+
class Meta:
|
54
|
+
model = Currency
|
55
|
+
fields = ['id', 'code', 'name', 'currency_type', 'currency_type_display', 'is_active']
|
@@ -0,0 +1,62 @@
|
|
1
|
+
"""
|
2
|
+
Payment serializers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rest_framework import serializers
|
6
|
+
from ..models import UniversalPayment
|
7
|
+
|
8
|
+
|
9
|
+
class UniversalPaymentSerializer(serializers.ModelSerializer):
|
10
|
+
"""Universal payment with status info."""
|
11
|
+
|
12
|
+
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
13
|
+
provider_display = serializers.CharField(source='get_provider_display', read_only=True)
|
14
|
+
is_pending = serializers.ReadOnlyField()
|
15
|
+
is_completed = serializers.ReadOnlyField()
|
16
|
+
is_failed = serializers.ReadOnlyField()
|
17
|
+
|
18
|
+
class Meta:
|
19
|
+
model = UniversalPayment
|
20
|
+
fields = [
|
21
|
+
'id', 'internal_payment_id', 'provider_payment_id', 'order_id',
|
22
|
+
'amount_usd', 'currency_code', 'actual_amount_usd', 'actual_currency_code',
|
23
|
+
'fee_amount_usd', 'provider', 'provider_display', 'status', 'status_display',
|
24
|
+
'pay_address', 'pay_amount', 'network', 'description',
|
25
|
+
'is_pending', 'is_completed', 'is_failed',
|
26
|
+
'expires_at', 'completed_at', 'created_at', 'updated_at'
|
27
|
+
]
|
28
|
+
read_only_fields = [
|
29
|
+
'id', 'internal_payment_id', 'provider_payment_id',
|
30
|
+
'actual_amount_usd', 'actual_currency_code', 'fee_amount_usd',
|
31
|
+
'pay_address', 'pay_amount', 'completed_at', 'processed_at',
|
32
|
+
'created_at', 'updated_at'
|
33
|
+
]
|
34
|
+
|
35
|
+
|
36
|
+
class PaymentCreateSerializer(serializers.ModelSerializer):
|
37
|
+
"""Create payment request."""
|
38
|
+
|
39
|
+
class Meta:
|
40
|
+
model = UniversalPayment
|
41
|
+
fields = [
|
42
|
+
'amount_usd', 'currency_code', 'provider', 'description', 'order_id'
|
43
|
+
]
|
44
|
+
|
45
|
+
def validate_amount_usd(self, value):
|
46
|
+
"""Validate payment amount."""
|
47
|
+
if value < 1.0:
|
48
|
+
raise serializers.ValidationError("Minimum payment amount is $1.00")
|
49
|
+
return value
|
50
|
+
|
51
|
+
|
52
|
+
class PaymentListSerializer(serializers.ModelSerializer):
|
53
|
+
"""Simplified payment for lists."""
|
54
|
+
|
55
|
+
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
56
|
+
|
57
|
+
class Meta:
|
58
|
+
model = UniversalPayment
|
59
|
+
fields = [
|
60
|
+
'id', 'internal_payment_id', 'amount_usd', 'currency_code',
|
61
|
+
'provider', 'status', 'status_display', 'description', 'created_at'
|
62
|
+
]
|
@@ -0,0 +1,71 @@
|
|
1
|
+
"""
|
2
|
+
Subscription serializers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rest_framework import serializers
|
6
|
+
from ..models import Subscription, EndpointGroup
|
7
|
+
|
8
|
+
|
9
|
+
class EndpointGroupSerializer(serializers.ModelSerializer):
|
10
|
+
"""Endpoint group with pricing tiers."""
|
11
|
+
|
12
|
+
class Meta:
|
13
|
+
model = EndpointGroup
|
14
|
+
fields = [
|
15
|
+
'id', 'name', 'display_name', 'description',
|
16
|
+
'basic_price', 'premium_price', 'enterprise_price',
|
17
|
+
'basic_limit', 'premium_limit', 'enterprise_limit',
|
18
|
+
'is_active', 'require_api_key'
|
19
|
+
]
|
20
|
+
|
21
|
+
|
22
|
+
class SubscriptionSerializer(serializers.ModelSerializer):
|
23
|
+
"""Subscription with computed fields."""
|
24
|
+
|
25
|
+
endpoint_group_name = serializers.CharField(source='endpoint_group.name', read_only=True)
|
26
|
+
endpoint_group_display = serializers.CharField(source='endpoint_group.display_name', read_only=True)
|
27
|
+
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
28
|
+
tier_display = serializers.CharField(source='get_tier_display', read_only=True)
|
29
|
+
is_active_subscription = serializers.ReadOnlyField(source='is_active')
|
30
|
+
is_usage_exceeded = serializers.ReadOnlyField()
|
31
|
+
|
32
|
+
class Meta:
|
33
|
+
model = Subscription
|
34
|
+
fields = [
|
35
|
+
'id', 'endpoint_group', 'endpoint_group_name', 'endpoint_group_display',
|
36
|
+
'tier', 'tier_display', 'status', 'status_display', 'monthly_price',
|
37
|
+
'usage_limit', 'usage_current', 'is_active_subscription', 'is_usage_exceeded',
|
38
|
+
'last_billed', 'next_billing', 'expires_at', 'created_at'
|
39
|
+
]
|
40
|
+
read_only_fields = [
|
41
|
+
'usage_current', 'last_billed', 'next_billing', 'cancelled_at', 'created_at'
|
42
|
+
]
|
43
|
+
|
44
|
+
|
45
|
+
class SubscriptionCreateSerializer(serializers.Serializer):
|
46
|
+
"""Create subscription request."""
|
47
|
+
|
48
|
+
endpoint_group_id = serializers.IntegerField()
|
49
|
+
tier = serializers.ChoiceField(choices=Subscription.SubscriptionTier.choices)
|
50
|
+
|
51
|
+
def validate_endpoint_group_id(self, value):
|
52
|
+
"""Validate endpoint group exists."""
|
53
|
+
try:
|
54
|
+
endpoint_group = EndpointGroup.objects.get(id=value, is_active=True)
|
55
|
+
return value
|
56
|
+
except EndpointGroup.DoesNotExist:
|
57
|
+
raise serializers.ValidationError("Endpoint group not found or inactive")
|
58
|
+
|
59
|
+
|
60
|
+
class SubscriptionListSerializer(serializers.ModelSerializer):
|
61
|
+
"""Simplified subscription for lists."""
|
62
|
+
|
63
|
+
endpoint_group_name = serializers.CharField(source='endpoint_group.name', read_only=True)
|
64
|
+
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
65
|
+
|
66
|
+
class Meta:
|
67
|
+
model = Subscription
|
68
|
+
fields = [
|
69
|
+
'id', 'endpoint_group_name', 'tier', 'status', 'status_display',
|
70
|
+
'monthly_price', 'usage_current', 'usage_limit', 'expires_at'
|
71
|
+
]
|
@@ -0,0 +1,56 @@
|
|
1
|
+
"""
|
2
|
+
Tariff serializers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rest_framework import serializers
|
6
|
+
from ..models import Tariff, TariffEndpointGroup
|
7
|
+
|
8
|
+
|
9
|
+
class TariffSerializer(serializers.ModelSerializer):
|
10
|
+
"""Tariff with pricing info."""
|
11
|
+
|
12
|
+
is_free = serializers.SerializerMethodField()
|
13
|
+
endpoint_groups_count = serializers.SerializerMethodField()
|
14
|
+
|
15
|
+
class Meta:
|
16
|
+
model = Tariff
|
17
|
+
fields = [
|
18
|
+
'id', 'name', 'display_name', 'description', 'monthly_price',
|
19
|
+
'request_limit', 'is_free', 'is_active', 'endpoint_groups_count'
|
20
|
+
]
|
21
|
+
|
22
|
+
def get_is_free(self, obj):
|
23
|
+
"""Check if tariff is free."""
|
24
|
+
return obj.is_free
|
25
|
+
|
26
|
+
def get_endpoint_groups_count(self, obj):
|
27
|
+
"""Get count of enabled endpoint groups."""
|
28
|
+
return obj.endpoint_groups.filter(is_enabled=True).count()
|
29
|
+
|
30
|
+
|
31
|
+
class TariffEndpointGroupSerializer(serializers.ModelSerializer):
|
32
|
+
"""Tariff endpoint group association."""
|
33
|
+
|
34
|
+
tariff_name = serializers.CharField(source='tariff.name', read_only=True)
|
35
|
+
endpoint_group_name = serializers.CharField(source='endpoint_group.name', read_only=True)
|
36
|
+
|
37
|
+
class Meta:
|
38
|
+
model = TariffEndpointGroup
|
39
|
+
fields = [
|
40
|
+
'id', 'tariff', 'tariff_name', 'endpoint_group', 'endpoint_group_name',
|
41
|
+
'is_enabled'
|
42
|
+
]
|
43
|
+
|
44
|
+
|
45
|
+
class TariffListSerializer(serializers.ModelSerializer):
|
46
|
+
"""Simplified tariff for lists."""
|
47
|
+
|
48
|
+
is_free = serializers.SerializerMethodField()
|
49
|
+
|
50
|
+
class Meta:
|
51
|
+
model = Tariff
|
52
|
+
fields = ['id', 'name', 'display_name', 'monthly_price', 'is_free', 'is_active']
|
53
|
+
|
54
|
+
def get_is_free(self, obj):
|
55
|
+
"""Check if tariff is free."""
|
56
|
+
return obj.is_free
|
@@ -0,0 +1,65 @@
|
|
1
|
+
"""
|
2
|
+
Universal Payment Services.
|
3
|
+
|
4
|
+
Modular architecture with minimal Pydantic typing for inter-service communication.
|
5
|
+
Uses Django ORM for data persistence and DRF for API responses.
|
6
|
+
"""
|
7
|
+
|
8
|
+
# Core services
|
9
|
+
from .core.payment_service import PaymentService
|
10
|
+
from .core.balance_service import BalanceService
|
11
|
+
from .core.subscription_service import SubscriptionService
|
12
|
+
|
13
|
+
# Provider services
|
14
|
+
from .providers.registry import ProviderRegistry
|
15
|
+
from .providers.nowpayments import NowPaymentsProvider
|
16
|
+
from .providers.cryptapi import CryptAPIProvider
|
17
|
+
|
18
|
+
# Cache services
|
19
|
+
from .cache import SimpleCache, ApiKeyCache, RateLimitCache
|
20
|
+
|
21
|
+
# Internal types for inter-service communication
|
22
|
+
from .internal_types import (
|
23
|
+
ProviderResponse, WebhookData, ServiceOperationResult,
|
24
|
+
BalanceUpdateRequest, AccessCheckRequest, AccessCheckResult,
|
25
|
+
# Service response models
|
26
|
+
PaymentCreationResult, WebhookProcessingResult, PaymentStatusResult,
|
27
|
+
UserBalanceResult, TransferResult, TransactionInfo,
|
28
|
+
EndpointGroupInfo, SubscriptionInfo, SubscriptionAnalytics
|
29
|
+
)
|
30
|
+
|
31
|
+
__all__ = [
|
32
|
+
# Core services
|
33
|
+
'PaymentService',
|
34
|
+
'BalanceService',
|
35
|
+
'SubscriptionService',
|
36
|
+
|
37
|
+
# Provider services
|
38
|
+
'ProviderRegistry',
|
39
|
+
'NowPaymentsProvider',
|
40
|
+
'CryptAPIProvider',
|
41
|
+
|
42
|
+
# Cache services
|
43
|
+
'SimpleCache',
|
44
|
+
'ApiKeyCache',
|
45
|
+
'RateLimitCache',
|
46
|
+
|
47
|
+
# Internal types
|
48
|
+
'ProviderResponse',
|
49
|
+
'WebhookData',
|
50
|
+
'ServiceOperationResult',
|
51
|
+
'BalanceUpdateRequest',
|
52
|
+
'AccessCheckRequest',
|
53
|
+
'AccessCheckResult',
|
54
|
+
|
55
|
+
# Service response models
|
56
|
+
'PaymentCreationResult',
|
57
|
+
'WebhookProcessingResult',
|
58
|
+
'PaymentStatusResult',
|
59
|
+
'UserBalanceResult',
|
60
|
+
'TransferResult',
|
61
|
+
'TransactionInfo',
|
62
|
+
'EndpointGroupInfo',
|
63
|
+
'SubscriptionInfo',
|
64
|
+
'SubscriptionAnalytics',
|
65
|
+
]
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
Simple caching for API key access control and rate limiting.
|
3
|
+
|
4
|
+
ONLY for API key caching - NOT for payment data!
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .base import CacheInterface
|
8
|
+
from .simple_cache import SimpleCache, ApiKeyCache, RateLimitCache
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
'CacheInterface',
|
12
|
+
'SimpleCache',
|
13
|
+
'ApiKeyCache',
|
14
|
+
'RateLimitCache',
|
15
|
+
]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"""
|
2
|
+
Base cache interface for payments module.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from typing import Optional, Any
|
7
|
+
|
8
|
+
|
9
|
+
class CacheInterface(ABC):
|
10
|
+
"""Abstract cache interface."""
|
11
|
+
|
12
|
+
@abstractmethod
|
13
|
+
def get(self, key: str) -> Optional[Any]:
|
14
|
+
"""Get value from cache."""
|
15
|
+
pass
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
|
19
|
+
"""Set value in cache."""
|
20
|
+
pass
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def delete(self, key: str) -> bool:
|
24
|
+
"""Delete value from cache."""
|
25
|
+
pass
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
def exists(self, key: str) -> bool:
|
29
|
+
"""Check if key exists in cache."""
|
30
|
+
pass
|
@@ -0,0 +1,135 @@
|
|
1
|
+
"""
|
2
|
+
Simple cache implementation for API keys and rate limiting.
|
3
|
+
ONLY for API access control - NOT payment data!
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import Optional, Any
|
8
|
+
from django.core.cache import cache
|
9
|
+
|
10
|
+
from .base import CacheInterface
|
11
|
+
from ...utils.config_utils import CacheConfigHelper
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class SimpleCache(CacheInterface):
|
17
|
+
"""
|
18
|
+
Simple cache implementation using Django's cache framework.
|
19
|
+
|
20
|
+
Falls back gracefully when cache is unavailable.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, prefix: str = "payments"):
|
24
|
+
self.prefix = prefix
|
25
|
+
# Use config helper to check if cache is enabled
|
26
|
+
self.enabled = CacheConfigHelper.is_cache_enabled()
|
27
|
+
|
28
|
+
def _make_key(self, key: str) -> str:
|
29
|
+
"""Create prefixed cache key."""
|
30
|
+
return f"{self.prefix}:{key}"
|
31
|
+
|
32
|
+
def get(self, key: str) -> Optional[Any]:
|
33
|
+
"""Get value from cache."""
|
34
|
+
if not self.enabled:
|
35
|
+
return None
|
36
|
+
|
37
|
+
try:
|
38
|
+
cache_key = self._make_key(key)
|
39
|
+
return cache.get(cache_key)
|
40
|
+
except Exception as e:
|
41
|
+
logger.warning(f"Cache get failed for key {key}: {e}")
|
42
|
+
return None
|
43
|
+
|
44
|
+
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
|
45
|
+
"""Set value in cache."""
|
46
|
+
if not self.enabled:
|
47
|
+
return False
|
48
|
+
|
49
|
+
try:
|
50
|
+
cache_key = self._make_key(key)
|
51
|
+
cache.set(cache_key, value, timeout)
|
52
|
+
return True
|
53
|
+
except Exception as e:
|
54
|
+
logger.warning(f"Cache set failed for key {key}: {e}")
|
55
|
+
return False
|
56
|
+
|
57
|
+
def delete(self, key: str) -> bool:
|
58
|
+
"""Delete value from cache."""
|
59
|
+
if not self.enabled:
|
60
|
+
return False
|
61
|
+
|
62
|
+
try:
|
63
|
+
cache_key = self._make_key(key)
|
64
|
+
cache.delete(cache_key)
|
65
|
+
return True
|
66
|
+
except Exception as e:
|
67
|
+
logger.warning(f"Cache delete failed for key {key}: {e}")
|
68
|
+
return False
|
69
|
+
|
70
|
+
def exists(self, key: str) -> bool:
|
71
|
+
"""Check if key exists in cache."""
|
72
|
+
if not self.enabled:
|
73
|
+
return False
|
74
|
+
|
75
|
+
try:
|
76
|
+
cache_key = self._make_key(key)
|
77
|
+
return cache.get(cache_key) is not None
|
78
|
+
except Exception as e:
|
79
|
+
logger.warning(f"Cache exists check failed for key {key}: {e}")
|
80
|
+
return False
|
81
|
+
|
82
|
+
|
83
|
+
class ApiKeyCache:
|
84
|
+
"""Specialized cache for API key operations."""
|
85
|
+
|
86
|
+
def __init__(self):
|
87
|
+
self.cache = SimpleCache("api_keys")
|
88
|
+
# Get timeout from config
|
89
|
+
self.default_timeout = CacheConfigHelper.get_cache_timeout('api_key')
|
90
|
+
|
91
|
+
def get_api_key_data(self, api_key: str) -> Optional[dict]:
|
92
|
+
"""Get cached API key data."""
|
93
|
+
return self.cache.get(f"key:{api_key}")
|
94
|
+
|
95
|
+
def cache_api_key_data(self, api_key: str, data: dict) -> bool:
|
96
|
+
"""Cache API key data."""
|
97
|
+
return self.cache.set(f"key:{api_key}", data, self.default_timeout)
|
98
|
+
|
99
|
+
def invalidate_api_key(self, api_key: str) -> bool:
|
100
|
+
"""Invalidate cached API key."""
|
101
|
+
return self.cache.delete(f"key:{api_key}")
|
102
|
+
|
103
|
+
|
104
|
+
class RateLimitCache:
|
105
|
+
"""Specialized cache for rate limiting."""
|
106
|
+
|
107
|
+
def __init__(self):
|
108
|
+
self.cache = SimpleCache("rate_limit")
|
109
|
+
|
110
|
+
def get_usage_count(self, user_id: int, endpoint_group: str, window: str = "hour") -> int:
|
111
|
+
"""Get current usage count for rate limiting."""
|
112
|
+
key = f"usage:{user_id}:{endpoint_group}:{window}"
|
113
|
+
count = self.cache.get(key)
|
114
|
+
return count if count is not None else 0
|
115
|
+
|
116
|
+
def increment_usage(self, user_id: int, endpoint_group: str, window: str = "hour", ttl: Optional[int] = None) -> int:
|
117
|
+
"""Increment usage count and return new count."""
|
118
|
+
key = f"usage:{user_id}:{endpoint_group}:{window}"
|
119
|
+
|
120
|
+
# Get current count
|
121
|
+
current = self.get_usage_count(user_id, endpoint_group, window)
|
122
|
+
new_count = current + 1
|
123
|
+
|
124
|
+
# Use config helper for TTL if not provided
|
125
|
+
if ttl is None:
|
126
|
+
ttl = CacheConfigHelper.get_cache_timeout('rate_limit')
|
127
|
+
|
128
|
+
# Set new count with TTL
|
129
|
+
self.cache.set(key, new_count, ttl)
|
130
|
+
return new_count
|
131
|
+
|
132
|
+
def reset_usage(self, user_id: int, endpoint_group: str, window: str = "hour") -> bool:
|
133
|
+
"""Reset usage count."""
|
134
|
+
key = f"usage:{user_id}:{endpoint_group}:{window}"
|
135
|
+
return self.cache.delete(key)
|