django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- 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/config/__init__.py +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -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 +125 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -7
- 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 +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -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 +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -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/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,296 @@
|
|
1
|
+
"""
|
2
|
+
Usage Tracking Middleware.
|
3
|
+
Tracks API usage for billing, analytics, and monitoring.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
import json
|
8
|
+
from typing import Optional, Dict, Any
|
9
|
+
from django.http import HttpRequest, HttpResponse
|
10
|
+
from django.utils.deprecation import MiddlewareMixin
|
11
|
+
from django.conf import settings
|
12
|
+
from django.utils import timezone
|
13
|
+
from ..models import APIKey, Subscription, Transaction
|
14
|
+
from ..services import RateLimitCache
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class UsageTrackingMiddleware(MiddlewareMixin):
|
20
|
+
"""
|
21
|
+
Middleware for tracking API usage and creating billing records.
|
22
|
+
|
23
|
+
Features:
|
24
|
+
- Request/response logging
|
25
|
+
- Usage analytics
|
26
|
+
- Billing event creation
|
27
|
+
- Performance monitoring
|
28
|
+
- Error tracking
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, get_response=None):
|
32
|
+
super().__init__(get_response)
|
33
|
+
self.redis_service = RedisService()
|
34
|
+
|
35
|
+
# Enable/disable usage tracking
|
36
|
+
self.enabled = getattr(settings, 'PAYMENTS_USAGE_TRACKING_ENABLED', True)
|
37
|
+
|
38
|
+
# Paths to track (empty means track all API paths)
|
39
|
+
self.tracked_paths = getattr(settings, 'PAYMENTS_TRACKED_PATHS', [])
|
40
|
+
|
41
|
+
# Paths to exclude from tracking
|
42
|
+
self.excluded_paths = getattr(settings, 'PAYMENTS_EXCLUDED_PATHS', [
|
43
|
+
'/admin/',
|
44
|
+
'/cfg/',
|
45
|
+
'/api/v1/api-key/validate/',
|
46
|
+
])
|
47
|
+
|
48
|
+
# Track request bodies (be careful with sensitive data)
|
49
|
+
self.track_request_body = getattr(settings, 'PAYMENTS_TRACK_REQUEST_BODY', False)
|
50
|
+
|
51
|
+
# Track response bodies (be careful with large responses)
|
52
|
+
self.track_response_body = getattr(settings, 'PAYMENTS_TRACK_RESPONSE_BODY', False)
|
53
|
+
|
54
|
+
def process_request(self, request: HttpRequest) -> None:
|
55
|
+
"""Process incoming request for usage tracking."""
|
56
|
+
|
57
|
+
if not self.enabled:
|
58
|
+
return
|
59
|
+
|
60
|
+
# Skip excluded paths
|
61
|
+
if self._is_excluded_path(request):
|
62
|
+
return
|
63
|
+
|
64
|
+
# Only track if we have API key
|
65
|
+
if not hasattr(request, 'payment_api_key'):
|
66
|
+
return
|
67
|
+
|
68
|
+
# Record request start time
|
69
|
+
request._usage_start_time = timezone.now()
|
70
|
+
|
71
|
+
# Prepare usage data
|
72
|
+
request._usage_data = {
|
73
|
+
'api_key_id': request.payment_api_key.id,
|
74
|
+
'user_id': request.payment_api_key.user.id,
|
75
|
+
'method': request.method,
|
76
|
+
'path': request.path,
|
77
|
+
'query_params': dict(request.GET),
|
78
|
+
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
79
|
+
'ip_address': self._get_client_ip(request),
|
80
|
+
'start_time': request._usage_start_time,
|
81
|
+
}
|
82
|
+
|
83
|
+
# Add subscription info if available
|
84
|
+
if hasattr(request, 'payment_subscription'):
|
85
|
+
request._usage_data.update({
|
86
|
+
'subscription_id': request.payment_subscription.id,
|
87
|
+
'endpoint_group_id': request.payment_subscription.endpoint_group.id,
|
88
|
+
'tier': request.payment_subscription.tier,
|
89
|
+
})
|
90
|
+
|
91
|
+
# Track request body if enabled and safe
|
92
|
+
if self.track_request_body and self._is_safe_to_track_body(request):
|
93
|
+
try:
|
94
|
+
request._usage_data['request_body'] = request.body.decode('utf-8')[:1000] # Limit size
|
95
|
+
except:
|
96
|
+
pass
|
97
|
+
|
98
|
+
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
99
|
+
"""Process response for usage tracking."""
|
100
|
+
|
101
|
+
if not self.enabled or not hasattr(request, '_usage_data'):
|
102
|
+
return response
|
103
|
+
|
104
|
+
try:
|
105
|
+
# Calculate response time
|
106
|
+
end_time = timezone.now()
|
107
|
+
response_time_ms = int((end_time - request._usage_start_time).total_seconds() * 1000)
|
108
|
+
|
109
|
+
# Update usage data
|
110
|
+
usage_data = request._usage_data
|
111
|
+
usage_data.update({
|
112
|
+
'end_time': end_time,
|
113
|
+
'response_time_ms': response_time_ms,
|
114
|
+
'status_code': response.status_code,
|
115
|
+
'response_size': len(response.content) if hasattr(response, 'content') else 0,
|
116
|
+
})
|
117
|
+
|
118
|
+
# Track response body if enabled and safe
|
119
|
+
if (self.track_response_body and
|
120
|
+
self._is_safe_to_track_response(response) and
|
121
|
+
response.status_code < 400):
|
122
|
+
try:
|
123
|
+
content = response.content.decode('utf-8')[:1000] # Limit size
|
124
|
+
usage_data['response_body'] = content
|
125
|
+
except:
|
126
|
+
pass
|
127
|
+
|
128
|
+
# Track error details for failed requests
|
129
|
+
if response.status_code >= 400:
|
130
|
+
usage_data['is_error'] = True
|
131
|
+
usage_data['error_category'] = self._categorize_error(response.status_code)
|
132
|
+
else:
|
133
|
+
usage_data['is_error'] = False
|
134
|
+
|
135
|
+
# Store in Redis for real-time analytics
|
136
|
+
self._store_usage_data(usage_data)
|
137
|
+
|
138
|
+
# Create billing transaction if needed
|
139
|
+
if hasattr(request, 'payment_subscription'):
|
140
|
+
self._create_billing_transaction(request.payment_subscription, usage_data)
|
141
|
+
|
142
|
+
# Log for debugging
|
143
|
+
logger.info(
|
144
|
+
f"API usage tracked - User: {usage_data['user_id']}, "
|
145
|
+
f"Path: {usage_data['path']}, "
|
146
|
+
f"Status: {usage_data['status_code']}, "
|
147
|
+
f"Time: {response_time_ms}ms"
|
148
|
+
)
|
149
|
+
|
150
|
+
except Exception as e:
|
151
|
+
logger.error(f"Error in usage tracking: {e}")
|
152
|
+
|
153
|
+
return response
|
154
|
+
|
155
|
+
def _is_excluded_path(self, request: HttpRequest) -> bool:
|
156
|
+
"""Check if path should be excluded from tracking."""
|
157
|
+
path = request.path
|
158
|
+
|
159
|
+
# Check excluded paths
|
160
|
+
for excluded in self.excluded_paths:
|
161
|
+
if path.startswith(excluded):
|
162
|
+
return True
|
163
|
+
|
164
|
+
# If tracked_paths is specified, only track those
|
165
|
+
if self.tracked_paths:
|
166
|
+
return not any(path.startswith(tracked) for tracked in self.tracked_paths)
|
167
|
+
|
168
|
+
return False
|
169
|
+
|
170
|
+
def _get_client_ip(self, request: HttpRequest) -> Optional[str]:
|
171
|
+
"""Get client IP address."""
|
172
|
+
|
173
|
+
# Check for forwarded headers first
|
174
|
+
forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
175
|
+
if forwarded_for:
|
176
|
+
return forwarded_for.split(',')[0].strip()
|
177
|
+
|
178
|
+
# Check for real IP header
|
179
|
+
real_ip = request.META.get('HTTP_X_REAL_IP')
|
180
|
+
if real_ip:
|
181
|
+
return real_ip
|
182
|
+
|
183
|
+
# Fallback to remote address
|
184
|
+
return request.META.get('REMOTE_ADDR')
|
185
|
+
|
186
|
+
def _is_safe_to_track_body(self, request: HttpRequest) -> bool:
|
187
|
+
"""Check if it's safe to track request body."""
|
188
|
+
|
189
|
+
# Don't track large bodies
|
190
|
+
content_length = request.META.get('CONTENT_LENGTH')
|
191
|
+
if content_length and int(content_length) > 10000: # 10KB limit
|
192
|
+
return False
|
193
|
+
|
194
|
+
# Don't track file uploads
|
195
|
+
content_type = request.META.get('CONTENT_TYPE', '')
|
196
|
+
if 'multipart/form-data' in content_type:
|
197
|
+
return False
|
198
|
+
|
199
|
+
# Don't track sensitive endpoints
|
200
|
+
sensitive_paths = ['/api/v1/api-key/', '/api/v1/payment/']
|
201
|
+
path = request.path
|
202
|
+
if any(path.startswith(sensitive) for sensitive in sensitive_paths):
|
203
|
+
return False
|
204
|
+
|
205
|
+
return True
|
206
|
+
|
207
|
+
def _is_safe_to_track_response(self, response: HttpResponse) -> bool:
|
208
|
+
"""Check if it's safe to track response body."""
|
209
|
+
|
210
|
+
# Don't track large responses
|
211
|
+
if hasattr(response, 'content') and len(response.content) > 10000: # 10KB limit
|
212
|
+
return False
|
213
|
+
|
214
|
+
# Only track JSON responses
|
215
|
+
content_type = response.get('Content-Type', '')
|
216
|
+
if 'application/json' not in content_type:
|
217
|
+
return False
|
218
|
+
|
219
|
+
return True
|
220
|
+
|
221
|
+
def _categorize_error(self, status_code: int) -> str:
|
222
|
+
"""Categorize error by status code."""
|
223
|
+
|
224
|
+
if 400 <= status_code < 500:
|
225
|
+
return 'client_error'
|
226
|
+
elif 500 <= status_code < 600:
|
227
|
+
return 'server_error'
|
228
|
+
else:
|
229
|
+
return 'unknown_error'
|
230
|
+
|
231
|
+
def _store_usage_data(self, usage_data: Dict[str, Any]):
|
232
|
+
"""Store usage data in Redis for analytics."""
|
233
|
+
|
234
|
+
try:
|
235
|
+
# Store daily usage stats
|
236
|
+
date_key = usage_data['start_time'].strftime('%Y-%m-%d')
|
237
|
+
user_id = usage_data['user_id']
|
238
|
+
|
239
|
+
# Increment counters
|
240
|
+
self.redis_service.increment_daily_usage(user_id, date_key)
|
241
|
+
|
242
|
+
if usage_data.get('subscription_id'):
|
243
|
+
self.redis_service.increment_subscription_usage(
|
244
|
+
usage_data['subscription_id'],
|
245
|
+
date_key
|
246
|
+
)
|
247
|
+
|
248
|
+
# Store performance metrics
|
249
|
+
if usage_data['response_time_ms'] > 0:
|
250
|
+
self.redis_service.record_response_time(
|
251
|
+
usage_data['path'],
|
252
|
+
usage_data['response_time_ms']
|
253
|
+
)
|
254
|
+
|
255
|
+
# Store error metrics
|
256
|
+
if usage_data.get('is_error'):
|
257
|
+
self.redis_service.increment_error_count(
|
258
|
+
usage_data['path'],
|
259
|
+
usage_data['status_code']
|
260
|
+
)
|
261
|
+
|
262
|
+
except Exception as e:
|
263
|
+
logger.error(f"Error storing usage data in Redis: {e}")
|
264
|
+
|
265
|
+
def _create_billing_transaction(self, subscription: Subscription, usage_data: Dict[str, Any]):
|
266
|
+
"""Create billing transaction for usage-based pricing."""
|
267
|
+
|
268
|
+
try:
|
269
|
+
# Only create transaction for successful requests
|
270
|
+
if usage_data.get('is_error'):
|
271
|
+
return
|
272
|
+
|
273
|
+
# Check if this endpoint has usage-based pricing
|
274
|
+
# For now, we'll create a small transaction for each API call
|
275
|
+
# This could be batched or calculated differently based on business logic
|
276
|
+
|
277
|
+
cost_per_request = 0.001 # $0.001 per request (example)
|
278
|
+
|
279
|
+
# Create transaction record
|
280
|
+
Transaction.objects.create(
|
281
|
+
user=subscription.user,
|
282
|
+
subscription=subscription,
|
283
|
+
transaction_type='debit',
|
284
|
+
amount_usd=-cost_per_request, # Negative for debit
|
285
|
+
description=f"API usage: {usage_data['method']} {usage_data['path']}",
|
286
|
+
metadata={
|
287
|
+
'api_call_id': f"{usage_data['api_key_id']}_{usage_data['start_time'].timestamp()}",
|
288
|
+
'endpoint': usage_data['path'],
|
289
|
+
'method': usage_data['method'],
|
290
|
+
'response_time_ms': usage_data['response_time_ms'],
|
291
|
+
'status_code': usage_data['status_code'],
|
292
|
+
}
|
293
|
+
)
|
294
|
+
|
295
|
+
except Exception as e:
|
296
|
+
logger.error(f"Error creating billing transaction: {e}")
|
@@ -1,7 +1,8 @@
|
|
1
|
-
# Generated by Django 5.2.6 on 2025-09-
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-24 07:15
|
2
2
|
|
3
3
|
import django.core.validators
|
4
4
|
import django.db.models.deletion
|
5
|
+
import uuid
|
5
6
|
from django.conf import settings
|
6
7
|
from django.db import migrations, models
|
7
8
|
|
@@ -172,8 +173,12 @@ class Migration(migrations.Migration):
|
|
172
173
|
fields=[
|
173
174
|
(
|
174
175
|
"id",
|
175
|
-
models.
|
176
|
-
|
176
|
+
models.UUIDField(
|
177
|
+
default=uuid.uuid4,
|
178
|
+
editable=False,
|
179
|
+
help_text="Unique identifier",
|
180
|
+
primary_key=True,
|
181
|
+
serialize=False,
|
177
182
|
),
|
178
183
|
),
|
179
184
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -252,8 +257,12 @@ class Migration(migrations.Migration):
|
|
252
257
|
fields=[
|
253
258
|
(
|
254
259
|
"id",
|
255
|
-
models.
|
256
|
-
|
260
|
+
models.UUIDField(
|
261
|
+
default=uuid.uuid4,
|
262
|
+
editable=False,
|
263
|
+
help_text="Unique identifier",
|
264
|
+
primary_key=True,
|
265
|
+
serialize=False,
|
257
266
|
),
|
258
267
|
),
|
259
268
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -442,8 +451,12 @@ class Migration(migrations.Migration):
|
|
442
451
|
fields=[
|
443
452
|
(
|
444
453
|
"id",
|
445
|
-
models.
|
446
|
-
|
454
|
+
models.UUIDField(
|
455
|
+
default=uuid.uuid4,
|
456
|
+
editable=False,
|
457
|
+
help_text="Unique identifier",
|
458
|
+
primary_key=True,
|
459
|
+
serialize=False,
|
447
460
|
),
|
448
461
|
),
|
449
462
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -485,6 +498,8 @@ class Migration(migrations.Migration):
|
|
485
498
|
models.CharField(
|
486
499
|
choices=[
|
487
500
|
("nowpayments", "NowPayments"),
|
501
|
+
("cryptapi", "CryptAPI"),
|
502
|
+
("cryptomus", "Cryptomus"),
|
488
503
|
("stripe", "Stripe"),
|
489
504
|
("internal", "Internal"),
|
490
505
|
],
|
@@ -564,6 +579,76 @@ class Migration(migrations.Migration):
|
|
564
579
|
blank=True, help_text="Raw webhook data from provider", null=True
|
565
580
|
),
|
566
581
|
),
|
582
|
+
(
|
583
|
+
"security_nonce",
|
584
|
+
models.CharField(
|
585
|
+
blank=True,
|
586
|
+
db_index=True,
|
587
|
+
help_text="Security nonce for replay attack protection (CryptAPI, Cryptomus, etc.)",
|
588
|
+
max_length=64,
|
589
|
+
null=True,
|
590
|
+
),
|
591
|
+
),
|
592
|
+
(
|
593
|
+
"provider_callback_url",
|
594
|
+
models.CharField(
|
595
|
+
blank=True,
|
596
|
+
help_text="Full callback URL with security parameters",
|
597
|
+
max_length=512,
|
598
|
+
null=True,
|
599
|
+
),
|
600
|
+
),
|
601
|
+
(
|
602
|
+
"transaction_hash",
|
603
|
+
models.CharField(
|
604
|
+
blank=True,
|
605
|
+
db_index=True,
|
606
|
+
help_text="Main transaction hash/ID (txid_in for CryptAPI, hash for Cryptomus)",
|
607
|
+
max_length=256,
|
608
|
+
null=True,
|
609
|
+
),
|
610
|
+
),
|
611
|
+
(
|
612
|
+
"confirmation_hash",
|
613
|
+
models.CharField(
|
614
|
+
blank=True,
|
615
|
+
help_text="Secondary transaction hash (txid_out for CryptAPI, confirmation for others)",
|
616
|
+
max_length=256,
|
617
|
+
null=True,
|
618
|
+
),
|
619
|
+
),
|
620
|
+
(
|
621
|
+
"sender_address",
|
622
|
+
models.CharField(
|
623
|
+
blank=True,
|
624
|
+
help_text="Sender address (address_in for CryptAPI, from_address for Cryptomus)",
|
625
|
+
max_length=200,
|
626
|
+
null=True,
|
627
|
+
),
|
628
|
+
),
|
629
|
+
(
|
630
|
+
"receiver_address",
|
631
|
+
models.CharField(
|
632
|
+
blank=True,
|
633
|
+
help_text="Receiver address (address_out for CryptAPI, to_address for Cryptomus)",
|
634
|
+
max_length=200,
|
635
|
+
null=True,
|
636
|
+
),
|
637
|
+
),
|
638
|
+
(
|
639
|
+
"crypto_amount",
|
640
|
+
models.FloatField(
|
641
|
+
blank=True,
|
642
|
+
help_text="Amount in cryptocurrency units (value_coin for CryptAPI, amount for Cryptomus)",
|
643
|
+
null=True,
|
644
|
+
),
|
645
|
+
),
|
646
|
+
(
|
647
|
+
"confirmations_count",
|
648
|
+
models.PositiveIntegerField(
|
649
|
+
default=0, help_text="Number of blockchain confirmations"
|
650
|
+
),
|
651
|
+
),
|
567
652
|
(
|
568
653
|
"expires_at",
|
569
654
|
models.DateTimeField(
|
@@ -606,8 +691,12 @@ class Migration(migrations.Migration):
|
|
606
691
|
fields=[
|
607
692
|
(
|
608
693
|
"id",
|
609
|
-
models.
|
610
|
-
|
694
|
+
models.UUIDField(
|
695
|
+
default=uuid.uuid4,
|
696
|
+
editable=False,
|
697
|
+
help_text="Unique identifier",
|
698
|
+
primary_key=True,
|
699
|
+
serialize=False,
|
611
700
|
),
|
612
701
|
),
|
613
702
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -767,8 +856,12 @@ class Migration(migrations.Migration):
|
|
767
856
|
fields=[
|
768
857
|
(
|
769
858
|
"id",
|
770
|
-
models.
|
771
|
-
|
859
|
+
models.UUIDField(
|
860
|
+
default=uuid.uuid4,
|
861
|
+
editable=False,
|
862
|
+
help_text="Unique identifier",
|
863
|
+
primary_key=True,
|
864
|
+
serialize=False,
|
772
865
|
),
|
773
866
|
),
|
774
867
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -943,6 +1036,27 @@ class Migration(migrations.Migration):
|
|
943
1036
|
model_name="universalpayment",
|
944
1037
|
index=models.Index(fields=["processed_at"], name="universal_p_process_1c8a1f_idx"),
|
945
1038
|
),
|
1039
|
+
migrations.AddIndex(
|
1040
|
+
model_name="universalpayment",
|
1041
|
+
index=models.Index(fields=["security_nonce"], name="universal_p_securit_4a38cc_idx"),
|
1042
|
+
),
|
1043
|
+
migrations.AddIndex(
|
1044
|
+
model_name="universalpayment",
|
1045
|
+
index=models.Index(fields=["transaction_hash"], name="universal_p_transac_8a27c6_idx"),
|
1046
|
+
),
|
1047
|
+
migrations.AddIndex(
|
1048
|
+
model_name="universalpayment",
|
1049
|
+
index=models.Index(
|
1050
|
+
fields=["confirmations_count"], name="universal_p_confirm_8df8c9_idx"
|
1051
|
+
),
|
1052
|
+
),
|
1053
|
+
migrations.AddIndex(
|
1054
|
+
model_name="universalpayment",
|
1055
|
+
index=models.Index(
|
1056
|
+
fields=["provider", "status", "confirmations_count"],
|
1057
|
+
name="universal_p_provide_3c8a34_idx",
|
1058
|
+
),
|
1059
|
+
),
|
946
1060
|
migrations.AddIndex(
|
947
1061
|
model_name="transaction",
|
948
1062
|
index=models.Index(
|
@@ -28,6 +28,15 @@ from .api_keys import APIKey
|
|
28
28
|
# Event sourcing
|
29
29
|
from .events import PaymentEvent
|
30
30
|
|
31
|
+
# TextChoices classes for external use (accessing inner classes)
|
32
|
+
CurrencyType = Currency.CurrencyType
|
33
|
+
PaymentStatus = UniversalPayment.PaymentStatus
|
34
|
+
PaymentProvider = UniversalPayment.PaymentProvider
|
35
|
+
TransactionType = Transaction.TransactionType
|
36
|
+
SubscriptionStatus = Subscription.SubscriptionStatus
|
37
|
+
SubscriptionTier = Subscription.SubscriptionTier
|
38
|
+
EventType = PaymentEvent.EventType
|
39
|
+
|
31
40
|
__all__ = [
|
32
41
|
# Base
|
33
42
|
'TimestampedModel',
|
@@ -46,4 +55,13 @@ __all__ = [
|
|
46
55
|
'TariffEndpointGroup',
|
47
56
|
'APIKey',
|
48
57
|
'PaymentEvent',
|
58
|
+
|
59
|
+
# TextChoices
|
60
|
+
'CurrencyType',
|
61
|
+
'PaymentStatus',
|
62
|
+
'PaymentProvider',
|
63
|
+
'TransactionType',
|
64
|
+
'SubscriptionStatus',
|
65
|
+
'SubscriptionTier',
|
66
|
+
'EventType',
|
49
67
|
]
|
@@ -5,12 +5,12 @@ API key models for the universal payments system.
|
|
5
5
|
from django.db import models
|
6
6
|
from django.contrib.auth import get_user_model
|
7
7
|
from django.utils import timezone
|
8
|
-
from .base import
|
8
|
+
from .base import UUIDTimestampedModel
|
9
9
|
|
10
10
|
User = get_user_model()
|
11
11
|
|
12
12
|
|
13
|
-
class APIKey(
|
13
|
+
class APIKey(UUIDTimestampedModel):
|
14
14
|
"""API keys for authentication and usage tracking."""
|
15
15
|
|
16
16
|
user = models.ForeignKey(
|
@@ -7,7 +7,7 @@ from django.contrib.auth import get_user_model
|
|
7
7
|
from django.core.validators import MinValueValidator
|
8
8
|
from django.core.exceptions import ValidationError
|
9
9
|
from django.utils import timezone
|
10
|
-
from .base import TimestampedModel
|
10
|
+
from .base import UUIDTimestampedModel, TimestampedModel
|
11
11
|
|
12
12
|
User = get_user_model()
|
13
13
|
|
@@ -89,7 +89,7 @@ class UserBalance(TimestampedModel):
|
|
89
89
|
}
|
90
90
|
|
91
91
|
|
92
|
-
class Transaction(
|
92
|
+
class Transaction(UUIDTimestampedModel):
|
93
93
|
"""Transaction history model."""
|
94
94
|
|
95
95
|
class TransactionType(models.TextChoices):
|
@@ -2,6 +2,7 @@
|
|
2
2
|
Base model classes for the universal payments system.
|
3
3
|
"""
|
4
4
|
|
5
|
+
import uuid
|
5
6
|
from django.db import models
|
6
7
|
|
7
8
|
|
@@ -10,5 +11,20 @@ class TimestampedModel(models.Model):
|
|
10
11
|
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
11
12
|
updated_at = models.DateTimeField(auto_now=True)
|
12
13
|
|
14
|
+
class Meta:
|
15
|
+
abstract = True
|
16
|
+
|
17
|
+
|
18
|
+
class UUIDTimestampedModel(models.Model):
|
19
|
+
"""Base model with UUID primary key and automatic timestamps."""
|
20
|
+
id = models.UUIDField(
|
21
|
+
primary_key=True,
|
22
|
+
default=uuid.uuid4,
|
23
|
+
editable=False,
|
24
|
+
help_text="Unique identifier"
|
25
|
+
)
|
26
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
27
|
+
updated_at = models.DateTimeField(auto_now=True)
|
28
|
+
|
13
29
|
class Meta:
|
14
30
|
abstract = True
|
@@ -3,10 +3,10 @@ Event sourcing models for the universal payments system.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from django.db import models
|
6
|
-
from .base import
|
6
|
+
from .base import UUIDTimestampedModel
|
7
7
|
|
8
8
|
|
9
|
-
class PaymentEvent(
|
9
|
+
class PaymentEvent(UUIDTimestampedModel):
|
10
10
|
"""Event sourcing for payment operations - immutable audit trail."""
|
11
11
|
|
12
12
|
class EventType(models.TextChoices):
|