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.
Files changed (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {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-23 13:36
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.BigAutoField(
176
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
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.BigAutoField(
256
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
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.BigAutoField(
446
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
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.BigAutoField(
610
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
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.BigAutoField(
771
- auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
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 TimestampedModel
8
+ from .base import UUIDTimestampedModel
9
9
 
10
10
  User = get_user_model()
11
11
 
12
12
 
13
- class APIKey(TimestampedModel):
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(TimestampedModel):
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 TimestampedModel
6
+ from .base import UUIDTimestampedModel
7
7
 
8
8
 
9
- class PaymentEvent(TimestampedModel):
9
+ class PaymentEvent(UUIDTimestampedModel):
10
10
  """Event sourcing for payment operations - immutable audit trail."""
11
11
 
12
12
  class EventType(models.TextChoices):