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,136 @@
|
|
1
|
+
"""
|
2
|
+
Universal payments module settings.
|
3
|
+
|
4
|
+
Core settings for the payments system including security, rate limiting, billing, etc.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Dict
|
8
|
+
from pydantic import BaseModel, Field
|
9
|
+
|
10
|
+
from .providers import PaymentProviderConfig
|
11
|
+
|
12
|
+
|
13
|
+
class SecuritySettings(BaseModel):
|
14
|
+
"""Security-related payment settings."""
|
15
|
+
api_key_length: int = Field(default=32, description="API key length in characters")
|
16
|
+
api_key_prefix: str = Field(default="ak_", description="API key prefix")
|
17
|
+
min_balance_threshold: float = Field(default=0.0, description="Minimum balance threshold")
|
18
|
+
auto_create_api_keys: bool = Field(default=True, description="Auto-create API keys for new users")
|
19
|
+
|
20
|
+
|
21
|
+
class RateLimitSettings(BaseModel):
|
22
|
+
"""Rate limiting settings."""
|
23
|
+
default_rate_limit_per_hour: int = Field(default=1000, description="Default API rate limit per hour")
|
24
|
+
default_rate_limit_per_day: int = Field(default=10000, description="Default API rate limit per day")
|
25
|
+
burst_limit_multiplier: float = Field(default=2.0, description="Burst limit multiplier")
|
26
|
+
sliding_window_size: int = Field(default=3600, description="Sliding window size in seconds")
|
27
|
+
|
28
|
+
|
29
|
+
class BillingSettings(BaseModel):
|
30
|
+
"""Billing and subscription settings."""
|
31
|
+
auto_bill_subscriptions: bool = Field(default=True, description="Automatically bill subscriptions")
|
32
|
+
billing_grace_period_hours: int = Field(default=24, description="Grace period for failed billing")
|
33
|
+
retry_failed_payments: bool = Field(default=True, description="Retry failed payments")
|
34
|
+
max_payment_retries: int = Field(default=3, description="Maximum payment retry attempts")
|
35
|
+
min_payment_amount_usd: float = Field(default=1.0, description="Minimum payment amount in USD")
|
36
|
+
max_payment_amount_usd: float = Field(default=50000.0, description="Maximum payment amount in USD")
|
37
|
+
|
38
|
+
|
39
|
+
class CacheSettings(BaseModel):
|
40
|
+
"""Cache timeout settings."""
|
41
|
+
cache_timeout_access_check: int = Field(default=60, description="Cache timeout for access checks (seconds)")
|
42
|
+
cache_timeout_user_balance: int = Field(default=300, description="Cache timeout for user balance (seconds)")
|
43
|
+
cache_timeout_subscriptions: int = Field(default=600, description="Cache timeout for subscriptions (seconds)")
|
44
|
+
cache_timeout_provider_status: int = Field(default=1800, description="Cache timeout for provider status (seconds)")
|
45
|
+
cache_timeout_currency_rates: int = Field(default=3600, description="Cache timeout for currency rates (seconds)")
|
46
|
+
|
47
|
+
|
48
|
+
class NotificationSettings(BaseModel):
|
49
|
+
"""Notification settings."""
|
50
|
+
send_payment_confirmations: bool = Field(default=True, description="Send payment confirmation emails")
|
51
|
+
send_subscription_renewals: bool = Field(default=True, description="Send subscription renewal notifications")
|
52
|
+
send_balance_alerts: bool = Field(default=True, description="Send low balance alerts")
|
53
|
+
send_api_key_alerts: bool = Field(default=True, description="Send API key security alerts")
|
54
|
+
webhook_timeout: int = Field(default=30, description="Webhook timeout in seconds")
|
55
|
+
|
56
|
+
|
57
|
+
class PaymentsSettings(BaseModel):
|
58
|
+
"""Universal payments module settings."""
|
59
|
+
|
60
|
+
# General settings
|
61
|
+
enabled: bool = Field(default=True, description="Enable payments module")
|
62
|
+
debug_mode: bool = Field(default=False, description="Enable debug mode for payments")
|
63
|
+
|
64
|
+
# Component settings
|
65
|
+
security: SecuritySettings = Field(default_factory=SecuritySettings)
|
66
|
+
rate_limits: RateLimitSettings = Field(default_factory=RateLimitSettings)
|
67
|
+
billing: BillingSettings = Field(default_factory=BillingSettings)
|
68
|
+
cache: CacheSettings = Field(default_factory=CacheSettings)
|
69
|
+
notifications: NotificationSettings = Field(default_factory=NotificationSettings)
|
70
|
+
|
71
|
+
# Provider configurations
|
72
|
+
providers: Dict[str, PaymentProviderConfig] = Field(default_factory=dict, description="Payment provider configurations")
|
73
|
+
|
74
|
+
# Feature flags
|
75
|
+
enable_crypto_payments: bool = Field(default=True, description="Enable cryptocurrency payments")
|
76
|
+
enable_fiat_payments: bool = Field(default=True, description="Enable fiat currency payments")
|
77
|
+
enable_subscription_system: bool = Field(default=True, description="Enable subscription system")
|
78
|
+
enable_balance_system: bool = Field(default=True, description="Enable user balance system")
|
79
|
+
enable_api_key_system: bool = Field(default=True, description="Enable API key system")
|
80
|
+
enable_webhook_processing: bool = Field(default=True, description="Enable webhook processing")
|
81
|
+
|
82
|
+
# Backwards compatibility properties
|
83
|
+
@property
|
84
|
+
def auto_create_api_keys(self) -> bool:
|
85
|
+
"""Backwards compatibility for auto_create_api_keys."""
|
86
|
+
return self.security.auto_create_api_keys
|
87
|
+
|
88
|
+
@property
|
89
|
+
def min_balance_threshold(self) -> float:
|
90
|
+
"""Backwards compatibility for min_balance_threshold."""
|
91
|
+
return self.security.min_balance_threshold
|
92
|
+
|
93
|
+
@property
|
94
|
+
def default_rate_limit_per_hour(self) -> int:
|
95
|
+
"""Backwards compatibility for default_rate_limit_per_hour."""
|
96
|
+
return self.rate_limits.default_rate_limit_per_hour
|
97
|
+
|
98
|
+
@property
|
99
|
+
def default_rate_limit_per_day(self) -> int:
|
100
|
+
"""Backwards compatibility for default_rate_limit_per_day."""
|
101
|
+
return self.rate_limits.default_rate_limit_per_day
|
102
|
+
|
103
|
+
@property
|
104
|
+
def api_key_length(self) -> int:
|
105
|
+
"""Backwards compatibility for api_key_length."""
|
106
|
+
return self.security.api_key_length
|
107
|
+
|
108
|
+
@property
|
109
|
+
def api_key_prefix(self) -> str:
|
110
|
+
"""Backwards compatibility for api_key_prefix."""
|
111
|
+
return self.security.api_key_prefix
|
112
|
+
|
113
|
+
@property
|
114
|
+
def auto_bill_subscriptions(self) -> bool:
|
115
|
+
"""Backwards compatibility for auto_bill_subscriptions."""
|
116
|
+
return self.billing.auto_bill_subscriptions
|
117
|
+
|
118
|
+
@property
|
119
|
+
def billing_grace_period_hours(self) -> int:
|
120
|
+
"""Backwards compatibility for billing_grace_period_hours."""
|
121
|
+
return self.billing.billing_grace_period_hours
|
122
|
+
|
123
|
+
@property
|
124
|
+
def cache_timeout_access_check(self) -> int:
|
125
|
+
"""Backwards compatibility for cache_timeout_access_check."""
|
126
|
+
return self.cache.cache_timeout_access_check
|
127
|
+
|
128
|
+
@property
|
129
|
+
def cache_timeout_user_balance(self) -> int:
|
130
|
+
"""Backwards compatibility for cache_timeout_user_balance."""
|
131
|
+
return self.cache.cache_timeout_user_balance
|
132
|
+
|
133
|
+
@property
|
134
|
+
def cache_timeout_subscriptions(self) -> int:
|
135
|
+
"""Backwards compatibility for cache_timeout_subscriptions."""
|
136
|
+
return self.cache.cache_timeout_subscriptions
|
@@ -0,0 +1,198 @@
|
|
1
|
+
"""
|
2
|
+
Configuration utility functions.
|
3
|
+
|
4
|
+
Helper functions for working with payment configurations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Optional, List, Dict, Any
|
8
|
+
import logging
|
9
|
+
|
10
|
+
from .module import PaymentsCfgModule
|
11
|
+
from .settings import PaymentsSettings
|
12
|
+
from .providers import PaymentProviderConfig, NowPaymentsConfig, StripeConfig, CryptAPIConfig
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
# Global payments configuration instance
|
17
|
+
_payments_config = PaymentsCfgModule()
|
18
|
+
|
19
|
+
|
20
|
+
def get_payments_config() -> PaymentsSettings:
|
21
|
+
"""Get current payments configuration."""
|
22
|
+
return _payments_config.get_config()
|
23
|
+
|
24
|
+
|
25
|
+
def get_provider_config(provider_name: str) -> Optional[PaymentProviderConfig]:
|
26
|
+
"""Get configuration for specific payment provider."""
|
27
|
+
config = get_payments_config()
|
28
|
+
return config.providers.get(provider_name)
|
29
|
+
|
30
|
+
|
31
|
+
def get_nowpayments_config() -> Optional[NowPaymentsConfig]:
|
32
|
+
"""Get NowPayments configuration."""
|
33
|
+
provider_config = get_provider_config('nowpayments')
|
34
|
+
if isinstance(provider_config, NowPaymentsConfig):
|
35
|
+
return provider_config
|
36
|
+
return None
|
37
|
+
|
38
|
+
|
39
|
+
def get_stripe_config() -> Optional[StripeConfig]:
|
40
|
+
"""Get Stripe configuration."""
|
41
|
+
provider_config = get_provider_config('stripe')
|
42
|
+
if isinstance(provider_config, StripeConfig):
|
43
|
+
return provider_config
|
44
|
+
return None
|
45
|
+
|
46
|
+
|
47
|
+
def get_cryptapi_config() -> Optional[CryptAPIConfig]:
|
48
|
+
"""Get CryptAPI configuration."""
|
49
|
+
provider_config = get_provider_config('cryptapi')
|
50
|
+
if isinstance(provider_config, CryptAPIConfig):
|
51
|
+
return provider_config
|
52
|
+
return None
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
def is_payments_enabled() -> bool:
|
58
|
+
"""Check if payments module is enabled."""
|
59
|
+
try:
|
60
|
+
config = get_payments_config()
|
61
|
+
return config.enabled
|
62
|
+
except Exception as e:
|
63
|
+
logger.warning(f"Error checking payments status: {e}")
|
64
|
+
return False
|
65
|
+
|
66
|
+
|
67
|
+
def is_feature_enabled(feature_name: str) -> bool:
|
68
|
+
"""Check if specific payment feature is enabled."""
|
69
|
+
try:
|
70
|
+
config = get_payments_config()
|
71
|
+
feature_map = {
|
72
|
+
'crypto_payments': config.enable_crypto_payments,
|
73
|
+
'fiat_payments': config.enable_fiat_payments,
|
74
|
+
'subscriptions': config.enable_subscription_system,
|
75
|
+
'balance': config.enable_balance_system,
|
76
|
+
'api_keys': config.enable_api_key_system,
|
77
|
+
'webhooks': config.enable_webhook_processing,
|
78
|
+
}
|
79
|
+
return feature_map.get(feature_name, False)
|
80
|
+
except Exception as e:
|
81
|
+
logger.warning(f"Error checking feature {feature_name}: {e}")
|
82
|
+
return False
|
83
|
+
|
84
|
+
|
85
|
+
def get_enabled_providers() -> List[str]:
|
86
|
+
"""Get list of enabled payment providers."""
|
87
|
+
try:
|
88
|
+
config = get_payments_config()
|
89
|
+
return [name for name, provider_config in config.providers.items() if provider_config.enabled]
|
90
|
+
except Exception as e:
|
91
|
+
logger.warning(f"Error getting enabled providers: {e}")
|
92
|
+
return []
|
93
|
+
|
94
|
+
|
95
|
+
def get_provider_settings(provider_name: str) -> Dict[str, Any]:
|
96
|
+
"""Get provider settings as dictionary for service initialization."""
|
97
|
+
try:
|
98
|
+
provider_config = get_provider_config(provider_name)
|
99
|
+
if provider_config:
|
100
|
+
return provider_config.get_config_dict()
|
101
|
+
return {}
|
102
|
+
except Exception as e:
|
103
|
+
logger.error(f"Error getting settings for provider {provider_name}: {e}")
|
104
|
+
return {}
|
105
|
+
|
106
|
+
|
107
|
+
def validate_provider_config(provider_name: str) -> bool:
|
108
|
+
"""Validate provider configuration."""
|
109
|
+
try:
|
110
|
+
provider_config = get_provider_config(provider_name)
|
111
|
+
if not provider_config:
|
112
|
+
return False
|
113
|
+
|
114
|
+
# Basic validation - check if API key exists
|
115
|
+
config_dict = provider_config.get_config_dict()
|
116
|
+
return bool(config_dict.get('api_key'))
|
117
|
+
|
118
|
+
except Exception as e:
|
119
|
+
logger.error(f"Error validating provider {provider_name}: {e}")
|
120
|
+
return False
|
121
|
+
|
122
|
+
|
123
|
+
def get_rate_limit_settings() -> Dict[str, int]:
|
124
|
+
"""Get rate limiting settings."""
|
125
|
+
try:
|
126
|
+
config = get_payments_config()
|
127
|
+
return {
|
128
|
+
'hourly_limit': config.rate_limits.default_rate_limit_per_hour,
|
129
|
+
'daily_limit': config.rate_limits.default_rate_limit_per_day,
|
130
|
+
'burst_multiplier': config.rate_limits.burst_limit_multiplier,
|
131
|
+
'window_size': config.rate_limits.sliding_window_size,
|
132
|
+
}
|
133
|
+
except Exception as e:
|
134
|
+
logger.warning(f"Error getting rate limit settings: {e}")
|
135
|
+
return {
|
136
|
+
'hourly_limit': 1000,
|
137
|
+
'daily_limit': 10000,
|
138
|
+
'burst_multiplier': 2.0,
|
139
|
+
'window_size': 3600,
|
140
|
+
}
|
141
|
+
|
142
|
+
|
143
|
+
def get_cache_settings() -> Dict[str, int]:
|
144
|
+
"""Get cache timeout settings."""
|
145
|
+
try:
|
146
|
+
config = get_payments_config()
|
147
|
+
return {
|
148
|
+
'access_check': config.cache.cache_timeout_access_check,
|
149
|
+
'user_balance': config.cache.cache_timeout_user_balance,
|
150
|
+
'subscriptions': config.cache.cache_timeout_subscriptions,
|
151
|
+
'provider_status': config.cache.cache_timeout_provider_status,
|
152
|
+
'currency_rates': config.cache.cache_timeout_currency_rates,
|
153
|
+
}
|
154
|
+
except Exception as e:
|
155
|
+
logger.warning(f"Error getting cache settings: {e}")
|
156
|
+
return {
|
157
|
+
'access_check': 60,
|
158
|
+
'user_balance': 300,
|
159
|
+
'subscriptions': 600,
|
160
|
+
'provider_status': 1800,
|
161
|
+
'currency_rates': 3600,
|
162
|
+
}
|
163
|
+
|
164
|
+
|
165
|
+
def get_billing_settings() -> Dict[str, Any]:
|
166
|
+
"""Get billing settings."""
|
167
|
+
try:
|
168
|
+
config = get_payments_config()
|
169
|
+
return {
|
170
|
+
'auto_bill': config.billing.auto_bill_subscriptions,
|
171
|
+
'grace_period_hours': config.billing.billing_grace_period_hours,
|
172
|
+
'retry_failed': config.billing.retry_failed_payments,
|
173
|
+
'max_retries': config.billing.max_payment_retries,
|
174
|
+
'min_amount_usd': config.billing.min_payment_amount_usd,
|
175
|
+
'max_amount_usd': config.billing.max_payment_amount_usd,
|
176
|
+
}
|
177
|
+
except Exception as e:
|
178
|
+
logger.warning(f"Error getting billing settings: {e}")
|
179
|
+
return {
|
180
|
+
'auto_bill': True,
|
181
|
+
'grace_period_hours': 24,
|
182
|
+
'retry_failed': True,
|
183
|
+
'max_retries': 3,
|
184
|
+
'min_amount_usd': 1.0,
|
185
|
+
'max_amount_usd': 50000.0,
|
186
|
+
}
|
187
|
+
|
188
|
+
|
189
|
+
def reset_config_cache():
|
190
|
+
"""Reset configuration cache (useful for testing)."""
|
191
|
+
global _payments_config
|
192
|
+
_payments_config.reset_cache()
|
193
|
+
|
194
|
+
|
195
|
+
def reload_config():
|
196
|
+
"""Reload configuration from project settings."""
|
197
|
+
reset_config_cache()
|
198
|
+
return get_payments_config()
|
@@ -0,0 +1,291 @@
|
|
1
|
+
"""
|
2
|
+
Decorators for API access control and endpoint registration.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import functools
|
6
|
+
import logging
|
7
|
+
from typing import Optional, List, Callable, Any
|
8
|
+
from django.http import JsonResponse
|
9
|
+
from django.conf import settings
|
10
|
+
from django.utils import timezone
|
11
|
+
from .models import EndpointGroup, Subscription
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def require_api_key(func: Callable) -> Callable:
|
17
|
+
"""
|
18
|
+
Decorator to require valid API key for function-based views.
|
19
|
+
Works with APIAccessMiddleware.
|
20
|
+
"""
|
21
|
+
@functools.wraps(func)
|
22
|
+
def wrapper(request, *args, **kwargs):
|
23
|
+
if not hasattr(request, 'payment_api_key'):
|
24
|
+
return JsonResponse({
|
25
|
+
'error': {
|
26
|
+
'code': 'MISSING_API_KEY',
|
27
|
+
'message': 'Valid API key required',
|
28
|
+
'timestamp': timezone.now().isoformat(),
|
29
|
+
}
|
30
|
+
}, status=401)
|
31
|
+
|
32
|
+
return func(request, *args, **kwargs)
|
33
|
+
|
34
|
+
return wrapper
|
35
|
+
|
36
|
+
|
37
|
+
def require_subscription(endpoint_group_name: str):
|
38
|
+
"""
|
39
|
+
Decorator to require active subscription for specific endpoint group.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
endpoint_group_name: Name of the endpoint group to check
|
43
|
+
"""
|
44
|
+
def decorator(func: Callable) -> Callable:
|
45
|
+
@functools.wraps(func)
|
46
|
+
def wrapper(request, *args, **kwargs):
|
47
|
+
# Check if middleware already validated subscription
|
48
|
+
if hasattr(request, 'payment_subscription'):
|
49
|
+
subscription = request.payment_subscription
|
50
|
+
if subscription.endpoint_group.name == endpoint_group_name:
|
51
|
+
return func(request, *args, **kwargs)
|
52
|
+
|
53
|
+
# If not validated by middleware, check manually
|
54
|
+
if not hasattr(request, 'payment_api_key'):
|
55
|
+
return JsonResponse({
|
56
|
+
'error': {
|
57
|
+
'code': 'MISSING_API_KEY',
|
58
|
+
'message': 'Valid API key required',
|
59
|
+
'timestamp': timezone.now().isoformat(),
|
60
|
+
}
|
61
|
+
}, status=401)
|
62
|
+
|
63
|
+
try:
|
64
|
+
endpoint_group = EndpointGroup.objects.get(
|
65
|
+
name=endpoint_group_name,
|
66
|
+
is_active=True
|
67
|
+
)
|
68
|
+
|
69
|
+
subscription = Subscription.objects.filter(
|
70
|
+
user=request.payment_api_key.user,
|
71
|
+
endpoint_group=endpoint_group,
|
72
|
+
status='active',
|
73
|
+
expires_at__gt=timezone.now()
|
74
|
+
).first()
|
75
|
+
|
76
|
+
if not subscription:
|
77
|
+
return JsonResponse({
|
78
|
+
'error': {
|
79
|
+
'code': 'NO_SUBSCRIPTION',
|
80
|
+
'message': f'Active subscription required for {endpoint_group.display_name}',
|
81
|
+
'timestamp': timezone.now().isoformat(),
|
82
|
+
}
|
83
|
+
}, status=403)
|
84
|
+
|
85
|
+
# Store subscription in request
|
86
|
+
request.payment_subscription = subscription
|
87
|
+
|
88
|
+
return func(request, *args, **kwargs)
|
89
|
+
|
90
|
+
except EndpointGroup.DoesNotExist:
|
91
|
+
logger.error(f"Endpoint group '{endpoint_group_name}' not found")
|
92
|
+
return JsonResponse({
|
93
|
+
'error': {
|
94
|
+
'code': 'INVALID_ENDPOINT_GROUP',
|
95
|
+
'message': 'Invalid endpoint group',
|
96
|
+
'timestamp': timezone.now().isoformat(),
|
97
|
+
}
|
98
|
+
}, status=500)
|
99
|
+
|
100
|
+
return wrapper
|
101
|
+
return decorator
|
102
|
+
|
103
|
+
|
104
|
+
def require_tier(minimum_tier: str):
|
105
|
+
"""
|
106
|
+
Decorator to require minimum subscription tier.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
minimum_tier: Minimum required tier (basic, premium, enterprise)
|
110
|
+
"""
|
111
|
+
tier_hierarchy = {
|
112
|
+
'basic': 1,
|
113
|
+
'premium': 2,
|
114
|
+
'enterprise': 3,
|
115
|
+
}
|
116
|
+
|
117
|
+
def decorator(func: Callable) -> Callable:
|
118
|
+
@functools.wraps(func)
|
119
|
+
def wrapper(request, *args, **kwargs):
|
120
|
+
if not hasattr(request, 'payment_subscription'):
|
121
|
+
return JsonResponse({
|
122
|
+
'error': {
|
123
|
+
'code': 'NO_SUBSCRIPTION',
|
124
|
+
'message': 'Active subscription required',
|
125
|
+
'timestamp': timezone.now().isoformat(),
|
126
|
+
}
|
127
|
+
}, status=403)
|
128
|
+
|
129
|
+
subscription = request.payment_subscription
|
130
|
+
current_tier_level = tier_hierarchy.get(subscription.tier, 0)
|
131
|
+
required_tier_level = tier_hierarchy.get(minimum_tier, 999)
|
132
|
+
|
133
|
+
if current_tier_level < required_tier_level:
|
134
|
+
return JsonResponse({
|
135
|
+
'error': {
|
136
|
+
'code': 'INSUFFICIENT_TIER',
|
137
|
+
'message': f'Tier {minimum_tier} or higher required',
|
138
|
+
'current_tier': subscription.tier,
|
139
|
+
'required_tier': minimum_tier,
|
140
|
+
'timestamp': timezone.now().isoformat(),
|
141
|
+
}
|
142
|
+
}, status=403)
|
143
|
+
|
144
|
+
return func(request, *args, **kwargs)
|
145
|
+
|
146
|
+
return wrapper
|
147
|
+
return decorator
|
148
|
+
|
149
|
+
|
150
|
+
def track_usage(cost_per_request: float = 0.0):
|
151
|
+
"""
|
152
|
+
Decorator to track API usage and deduct costs.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
cost_per_request: Cost to deduct per successful request
|
156
|
+
"""
|
157
|
+
def decorator(func: Callable) -> Callable:
|
158
|
+
@functools.wraps(func)
|
159
|
+
def wrapper(request, *args, **kwargs):
|
160
|
+
# Execute the function
|
161
|
+
response = func(request, *args, **kwargs)
|
162
|
+
|
163
|
+
# Track usage if successful and we have subscription
|
164
|
+
if (hasattr(request, 'payment_subscription') and
|
165
|
+
hasattr(response, 'status_code') and
|
166
|
+
200 <= response.status_code < 300 and
|
167
|
+
cost_per_request > 0):
|
168
|
+
|
169
|
+
try:
|
170
|
+
from .models import Transaction
|
171
|
+
|
172
|
+
subscription = request.payment_subscription
|
173
|
+
|
174
|
+
# Create billing transaction
|
175
|
+
Transaction.objects.create(
|
176
|
+
user=subscription.user,
|
177
|
+
subscription=subscription,
|
178
|
+
transaction_type='debit',
|
179
|
+
amount_usd=-cost_per_request,
|
180
|
+
description=f"API usage: {request.method} {request.path}",
|
181
|
+
metadata={
|
182
|
+
'endpoint': request.path,
|
183
|
+
'method': request.method,
|
184
|
+
'cost_per_request': cost_per_request,
|
185
|
+
}
|
186
|
+
)
|
187
|
+
|
188
|
+
except Exception as e:
|
189
|
+
logger.error(f"Error tracking usage: {e}")
|
190
|
+
|
191
|
+
return response
|
192
|
+
|
193
|
+
return wrapper
|
194
|
+
return decorator
|
195
|
+
|
196
|
+
|
197
|
+
def register_endpoint(endpoint_group_name: str,
|
198
|
+
display_name: Optional[str] = None,
|
199
|
+
description: Optional[str] = None,
|
200
|
+
require_api_key: bool = True):
|
201
|
+
"""
|
202
|
+
Decorator to automatically register endpoint with the system.
|
203
|
+
This creates or updates EndpointGroup records.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
endpoint_group_name: Internal name for the endpoint group
|
207
|
+
display_name: Human-readable name
|
208
|
+
description: Description of the endpoint functionality
|
209
|
+
require_api_key: Whether this endpoint requires API key
|
210
|
+
"""
|
211
|
+
def decorator(func: Callable) -> Callable:
|
212
|
+
@functools.wraps(func)
|
213
|
+
def wrapper(*args, **kwargs):
|
214
|
+
# Auto-register endpoint group if it doesn't exist
|
215
|
+
try:
|
216
|
+
endpoint_group, created = EndpointGroup.objects.get_or_create(
|
217
|
+
name=endpoint_group_name,
|
218
|
+
defaults={
|
219
|
+
'display_name': display_name or endpoint_group_name.replace('_', ' ').title(),
|
220
|
+
'description': description or f'Auto-registered endpoint group: {endpoint_group_name}',
|
221
|
+
'require_api_key': require_api_key,
|
222
|
+
'is_active': True,
|
223
|
+
}
|
224
|
+
)
|
225
|
+
|
226
|
+
if created:
|
227
|
+
logger.info(f"Auto-registered endpoint group: {endpoint_group_name}")
|
228
|
+
|
229
|
+
except Exception as e:
|
230
|
+
logger.error(f"Error auto-registering endpoint group: {e}")
|
231
|
+
|
232
|
+
return func(*args, **kwargs)
|
233
|
+
|
234
|
+
return wrapper
|
235
|
+
return decorator
|
236
|
+
|
237
|
+
|
238
|
+
def check_usage_limit(func: Callable) -> Callable:
|
239
|
+
"""
|
240
|
+
Decorator to check subscription usage limits before processing request.
|
241
|
+
"""
|
242
|
+
@functools.wraps(func)
|
243
|
+
def wrapper(request, *args, **kwargs):
|
244
|
+
if hasattr(request, 'payment_subscription'):
|
245
|
+
subscription = request.payment_subscription
|
246
|
+
|
247
|
+
# Check if usage limit exceeded
|
248
|
+
if (subscription.usage_limit > 0 and
|
249
|
+
subscription.usage_current >= subscription.usage_limit):
|
250
|
+
|
251
|
+
return JsonResponse({
|
252
|
+
'error': {
|
253
|
+
'code': 'USAGE_EXCEEDED',
|
254
|
+
'message': 'Monthly usage limit exceeded',
|
255
|
+
'current_usage': subscription.usage_current,
|
256
|
+
'usage_limit': subscription.usage_limit,
|
257
|
+
'reset_date': subscription.next_billing.isoformat() if subscription.next_billing else None,
|
258
|
+
'timestamp': timezone.now().isoformat(),
|
259
|
+
}
|
260
|
+
}, status=429)
|
261
|
+
|
262
|
+
return func(request, *args, **kwargs)
|
263
|
+
|
264
|
+
return wrapper
|
265
|
+
|
266
|
+
|
267
|
+
# Utility decorator combinations
|
268
|
+
def api_endpoint(endpoint_group_name: str,
|
269
|
+
minimum_tier: str = 'basic',
|
270
|
+
cost_per_request: float = 0.0):
|
271
|
+
"""
|
272
|
+
Combination decorator for typical API endpoint protection.
|
273
|
+
|
274
|
+
Args:
|
275
|
+
endpoint_group_name: Name of the endpoint group
|
276
|
+
minimum_tier: Minimum subscription tier required
|
277
|
+
cost_per_request: Cost to charge per successful request
|
278
|
+
"""
|
279
|
+
def decorator(func: Callable) -> Callable:
|
280
|
+
# Apply decorators in reverse order (they wrap from inside out)
|
281
|
+
decorated_func = func
|
282
|
+
decorated_func = track_usage(cost_per_request)(decorated_func)
|
283
|
+
decorated_func = check_usage_limit(decorated_func)
|
284
|
+
decorated_func = require_tier(minimum_tier)(decorated_func)
|
285
|
+
decorated_func = require_subscription(endpoint_group_name)(decorated_func)
|
286
|
+
decorated_func = require_api_key(decorated_func)
|
287
|
+
decorated_func = register_endpoint(endpoint_group_name)(decorated_func)
|
288
|
+
|
289
|
+
return decorated_func
|
290
|
+
|
291
|
+
return decorator
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""
|
2
|
+
Django model managers for universal payments.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .payment_manager import UniversalPaymentManager
|
6
|
+
from .balance_manager import UserBalanceManager
|
7
|
+
from .subscription_manager import SubscriptionManager, EndpointGroupManager
|
8
|
+
from .tariff_manager import TariffManager, TariffEndpointGroupManager
|
9
|
+
from .api_key_manager import APIKeyManager
|
10
|
+
from .currency_manager import CurrencyManager, CurrencyNetworkManager
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
'UniversalPaymentManager',
|
14
|
+
'UserBalanceManager',
|
15
|
+
'SubscriptionManager',
|
16
|
+
'EndpointGroupManager',
|
17
|
+
'TariffManager',
|
18
|
+
'TariffEndpointGroupManager',
|
19
|
+
'APIKeyManager',
|
20
|
+
'CurrencyManager',
|
21
|
+
'CurrencyNetworkManager',
|
22
|
+
]
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""
|
2
|
+
API key managers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.db import models
|
6
|
+
from django.utils import timezone
|
7
|
+
|
8
|
+
|
9
|
+
class APIKeyManager(models.Manager):
|
10
|
+
"""Manager for APIKey model."""
|
11
|
+
|
12
|
+
def get_active_keys(self, user=None):
|
13
|
+
"""Get active API keys."""
|
14
|
+
queryset = self.filter(is_active=True)
|
15
|
+
if user:
|
16
|
+
queryset = queryset.filter(user=user)
|
17
|
+
return queryset
|
18
|
+
|
19
|
+
def get_expired_keys(self):
|
20
|
+
"""Get expired API keys."""
|
21
|
+
return self.filter(
|
22
|
+
expires_at__lte=timezone.now()
|
23
|
+
)
|
24
|
+
|
25
|
+
def get_valid_keys(self, user=None):
|
26
|
+
"""Get valid (active and not expired) API keys."""
|
27
|
+
now = timezone.now()
|
28
|
+
queryset = self.filter(
|
29
|
+
is_active=True
|
30
|
+
).filter(
|
31
|
+
models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now)
|
32
|
+
)
|
33
|
+
if user:
|
34
|
+
queryset = queryset.filter(user=user)
|
35
|
+
return queryset
|