django-cfg 1.2.23__py3-none-any.whl → 1.2.27__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 (85) 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/config/__init__.py +15 -37
  7. django_cfg/apps/payments/config/module.py +30 -122
  8. django_cfg/apps/payments/config/providers.py +28 -16
  9. django_cfg/apps/payments/config/settings.py +53 -93
  10. django_cfg/apps/payments/config/utils.py +10 -156
  11. django_cfg/apps/payments/management/__init__.py +3 -0
  12. django_cfg/apps/payments/management/commands/README.md +178 -0
  13. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  14. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  15. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  16. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  17. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  18. django_cfg/apps/payments/middleware/api_access.py +33 -0
  19. django_cfg/apps/payments/migrations/0001_initial.py +94 -1
  20. django_cfg/apps/payments/models/payments.py +110 -0
  21. django_cfg/apps/payments/services/__init__.py +7 -1
  22. django_cfg/apps/payments/services/core/balance_service.py +14 -16
  23. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  24. django_cfg/apps/payments/services/core/payment_service.py +212 -29
  25. django_cfg/apps/payments/services/core/subscription_service.py +15 -17
  26. django_cfg/apps/payments/services/internal_types.py +31 -0
  27. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  28. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  29. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  30. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  31. django_cfg/apps/payments/services/providers/cryptapi.py +14 -3
  32. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  33. django_cfg/apps/payments/services/providers/registry.py +4 -0
  34. django_cfg/apps/payments/services/security/__init__.py +34 -0
  35. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  36. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  37. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  38. django_cfg/apps/payments/signals/api_key_signals.py +10 -0
  39. django_cfg/apps/payments/signals/payment_signals.py +3 -2
  40. django_cfg/apps/payments/tasks/__init__.py +12 -0
  41. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  42. django_cfg/apps/payments/utils/__init__.py +7 -4
  43. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  44. django_cfg/apps/payments/utils/config_utils.py +2 -0
  45. django_cfg/apps/payments/views/payment_views.py +40 -2
  46. django_cfg/apps/payments/views/webhook_views.py +266 -0
  47. django_cfg/apps/payments/viewsets.py +65 -0
  48. django_cfg/cli/README.md +2 -2
  49. django_cfg/cli/commands/create_project.py +1 -1
  50. django_cfg/cli/commands/info.py +1 -1
  51. django_cfg/cli/main.py +1 -1
  52. django_cfg/cli/utils.py +5 -5
  53. django_cfg/core/config.py +18 -4
  54. django_cfg/models/payments.py +547 -0
  55. django_cfg/models/tasks.py +51 -2
  56. django_cfg/modules/base.py +11 -5
  57. django_cfg/modules/django_currency/README.md +104 -269
  58. django_cfg/modules/django_currency/__init__.py +99 -41
  59. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  60. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  61. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  62. django_cfg/modules/django_currency/core/__init__.py +42 -0
  63. django_cfg/modules/django_currency/core/converter.py +169 -0
  64. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  65. django_cfg/modules/django_currency/core/models.py +54 -0
  66. django_cfg/modules/django_currency/database/__init__.py +25 -0
  67. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  68. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  69. django_cfg/modules/django_currency/utils/cache.py +92 -0
  70. django_cfg/registry/core.py +10 -0
  71. django_cfg/template_archive/__init__.py +0 -0
  72. django_cfg/template_archive/django_sample.zip +0 -0
  73. {django_cfg-1.2.23.dist-info → django_cfg-1.2.27.dist-info}/METADATA +10 -6
  74. {django_cfg-1.2.23.dist-info → django_cfg-1.2.27.dist-info}/RECORD +77 -51
  75. django_cfg/apps/agents/examples/__init__.py +0 -3
  76. django_cfg/apps/agents/examples/simple_example.py +0 -161
  77. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  78. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  79. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  80. django_cfg/modules/django_currency/cache.py +0 -430
  81. django_cfg/modules/django_currency/converter.py +0 -324
  82. django_cfg/modules/django_currency/service.py +0 -277
  83. {django_cfg-1.2.23.dist-info → django_cfg-1.2.27.dist-info}/WHEEL +0 -0
  84. {django_cfg-1.2.23.dist-info → django_cfg-1.2.27.dist-info}/entry_points.txt +0 -0
  85. {django_cfg-1.2.23.dist-info → django_cfg-1.2.27.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,336 @@
1
+ """
2
+ Management command to update currency data using django_currency database loader.
3
+
4
+ This command automatically populates and updates the payments Currency model
5
+ with fresh data from external APIs (CoinGecko, YFinance).
6
+
7
+ Usage:
8
+ python manage.py update_currencies
9
+ python manage.py update_currencies --max-crypto 100 --max-fiat 20
10
+ python manage.py update_currencies --dry-run
11
+ python manage.py update_currencies --force-update
12
+ """
13
+
14
+ import logging
15
+ from typing import Dict, List, Optional
16
+ from datetime import datetime, timedelta
17
+
18
+ from django.core.management.base import BaseCommand, CommandError
19
+ from django.db import transaction
20
+ from django.utils import timezone
21
+ from django.conf import settings
22
+
23
+ from django_cfg.modules.django_currency.database.database_loader import (
24
+ CurrencyDatabaseLoader,
25
+ DatabaseLoaderConfig,
26
+ create_database_loader
27
+ )
28
+ from django_cfg.apps.payments.models.currencies import Currency
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class Command(BaseCommand):
34
+ """
35
+ Management command to update currency data.
36
+
37
+ Features:
38
+ - Automatic detection of new currencies
39
+ - Rate updates for existing currencies
40
+ - Dry-run mode for testing
41
+ - Configurable limits for API calls
42
+ - Progress reporting
43
+ - Error handling and rollback
44
+ """
45
+
46
+ help = 'Update currency data from external APIs (CoinGecko, YFinance)'
47
+
48
+ def add_arguments(self, parser):
49
+ """Add command line arguments."""
50
+ parser.add_argument(
51
+ '--max-crypto',
52
+ type=int,
53
+ default=500,
54
+ help='Maximum number of cryptocurrencies to load (default: 500)'
55
+ )
56
+
57
+ parser.add_argument(
58
+ '--max-fiat',
59
+ type=int,
60
+ default=50,
61
+ help='Maximum number of fiat currencies to load (default: 50)'
62
+ )
63
+
64
+ parser.add_argument(
65
+ '--min-market-cap',
66
+ type=float,
67
+ default=1000000,
68
+ help='Minimum market cap in USD for cryptocurrencies (default: 1M)'
69
+ )
70
+
71
+ parser.add_argument(
72
+ '--dry-run',
73
+ action='store_true',
74
+ help='Show what would be updated without making changes'
75
+ )
76
+
77
+ parser.add_argument(
78
+ '--force-update',
79
+ action='store_true',
80
+ help='Force update all currencies even if recently updated'
81
+ )
82
+
83
+ parser.add_argument(
84
+ '--exclude-stablecoins',
85
+ action='store_true',
86
+ help='Exclude stablecoins from cryptocurrency updates'
87
+ )
88
+
89
+ parser.add_argument(
90
+ '--update-threshold-hours',
91
+ type=int,
92
+ default=6,
93
+ help='Only update currencies older than N hours (default: 6)'
94
+ )
95
+
96
+ parser.add_argument(
97
+ '--verbose',
98
+ action='store_true',
99
+ help='Verbose output with detailed progress'
100
+ )
101
+
102
+ def handle(self, *args, **options):
103
+ """Main command handler."""
104
+
105
+ # Configure logging
106
+ log_level = logging.INFO if options['verbose'] else logging.WARNING
107
+ logging.getLogger('django_cfg.modules.django_currency').setLevel(log_level)
108
+
109
+ self.stdout.write(
110
+ self.style.SUCCESS('🏦 Starting currency database update...')
111
+ )
112
+
113
+ try:
114
+ # Create database loader with options
115
+ config = DatabaseLoaderConfig(
116
+ max_cryptocurrencies=options['max_crypto'],
117
+ max_fiat_currencies=options['max_fiat'],
118
+ min_market_cap_usd=options['min_market_cap'],
119
+ exclude_stablecoins=options['exclude_stablecoins'],
120
+ coingecko_delay=1.5, # Be respectful to APIs
121
+ yfinance_delay=0.5
122
+ )
123
+
124
+ loader = CurrencyDatabaseLoader(config)
125
+
126
+ # Get statistics
127
+ stats = loader.get_statistics()
128
+ self.stdout.write(f"📊 Loader config: {stats['total_currencies']} currencies available")
129
+ self.stdout.write(f" • {stats['total_fiat_currencies']} fiat currencies")
130
+ self.stdout.write(f" • {stats['total_cryptocurrencies']} cryptocurrencies")
131
+
132
+ # Check existing currencies
133
+ existing_count = Currency.objects.count()
134
+ self.stdout.write(f"📋 Current database: {existing_count} currencies")
135
+
136
+ # Determine update strategy
137
+ if options['force_update']:
138
+ currencies_to_update = Currency.objects.all()
139
+ self.stdout.write("🔄 Force update mode: updating all currencies")
140
+ else:
141
+ threshold = timezone.now() - timedelta(hours=options['update_threshold_hours'])
142
+ currencies_to_update = Currency.objects.filter(
143
+ rate_updated_at__lt=threshold
144
+ ) | Currency.objects.filter(rate_updated_at__isnull=True)
145
+
146
+ self.stdout.write(
147
+ f"⏰ Updating currencies older than {options['update_threshold_hours']} hours: "
148
+ f"{currencies_to_update.count()} currencies"
149
+ )
150
+
151
+ # Load fresh currency data
152
+ self.stdout.write("🌐 Fetching fresh currency data from APIs...")
153
+ fresh_currencies = loader.build_currency_database_data()
154
+
155
+ if options['dry_run']:
156
+ self._handle_dry_run(fresh_currencies, currencies_to_update)
157
+ else:
158
+ self._handle_update(fresh_currencies, currencies_to_update, options)
159
+
160
+ except KeyboardInterrupt:
161
+ self.stdout.write(
162
+ self.style.WARNING('\n⚠️ Update interrupted by user')
163
+ )
164
+ raise CommandError("Update cancelled by user")
165
+
166
+ except Exception as e:
167
+ logger.exception("Currency update failed")
168
+ self.stdout.write(
169
+ self.style.ERROR(f'❌ Currency update failed: {str(e)}')
170
+ )
171
+ raise CommandError(f"Update failed: {str(e)}")
172
+
173
+ def _handle_dry_run(self, fresh_currencies: List, currencies_to_update):
174
+ """Handle dry-run mode - show what would be updated."""
175
+ self.stdout.write(
176
+ self.style.WARNING('🧪 DRY RUN MODE - No changes will be made')
177
+ )
178
+
179
+ # Analyze changes
180
+ existing_codes = set(Currency.objects.values_list('code', flat=True))
181
+ fresh_codes = {curr.code for curr in fresh_currencies}
182
+
183
+ new_currencies = fresh_codes - existing_codes
184
+ existing_currencies = fresh_codes & existing_codes
185
+
186
+ self.stdout.write(f"\n📈 Analysis:")
187
+ self.stdout.write(f" • Would add {len(new_currencies)} new currencies")
188
+ self.stdout.write(f" • Would update {len(existing_currencies)} existing currencies")
189
+
190
+ if new_currencies:
191
+ self.stdout.write(f"\n➕ New currencies to add:")
192
+ for code in sorted(list(new_currencies)[:10]): # Show first 10
193
+ currency = next(c for c in fresh_currencies if c.code == code)
194
+ self.stdout.write(f" • {code}: {currency.name} ({currency.currency_type})")
195
+ if len(new_currencies) > 10:
196
+ self.stdout.write(f" ... and {len(new_currencies) - 10} more")
197
+
198
+ if existing_currencies:
199
+ self.stdout.write(f"\n🔄 Existing currencies to update:")
200
+ for code in sorted(list(existing_currencies)[:10]): # Show first 10
201
+ currency = next(c for c in fresh_currencies if c.code == code)
202
+ try:
203
+ existing = Currency.objects.get(code=code)
204
+ rate_diff = abs(existing.usd_rate - currency.usd_rate)
205
+ if rate_diff > 0.01: # Significant change
206
+ change_pct = ((currency.usd_rate - existing.usd_rate) / existing.usd_rate) * 100
207
+ self.stdout.write(
208
+ f" • {code}: ${existing.usd_rate:.6f} → ${currency.usd_rate:.6f} "
209
+ f"({change_pct:+.2f}%)"
210
+ )
211
+ except Currency.DoesNotExist:
212
+ pass
213
+
214
+ self.stdout.write(
215
+ self.style.SUCCESS('\n✅ Dry run completed - use --force-update to apply changes')
216
+ )
217
+
218
+ def _handle_update(self, fresh_currencies: List, currencies_to_update, options: Dict):
219
+ """Handle actual database update."""
220
+
221
+ updated_count = 0
222
+ created_count = 0
223
+ errors = []
224
+
225
+ # Create lookup for fresh data
226
+ fresh_data_map = {curr.code: curr for curr in fresh_currencies}
227
+
228
+ try:
229
+ with transaction.atomic():
230
+ self.stdout.write("💾 Updating database...")
231
+
232
+ # Process currencies
233
+ for i, fresh_currency in enumerate(fresh_currencies):
234
+ try:
235
+ currency, created = Currency.objects.update_or_create(
236
+ code=fresh_currency.code,
237
+ defaults={
238
+ 'name': fresh_currency.name,
239
+ 'symbol': fresh_currency.symbol,
240
+ 'currency_type': fresh_currency.currency_type,
241
+ 'decimal_places': fresh_currency.decimal_places,
242
+ 'usd_rate': fresh_currency.usd_rate,
243
+ 'min_payment_amount': fresh_currency.min_payment_amount,
244
+ 'is_active': fresh_currency.is_active,
245
+ 'rate_updated_at': timezone.now()
246
+ }
247
+ )
248
+
249
+ if created:
250
+ created_count += 1
251
+ if options['verbose']:
252
+ self.stdout.write(f" ➕ Created {fresh_currency.code}")
253
+ else:
254
+ updated_count += 1
255
+ if options['verbose']:
256
+ self.stdout.write(f" 🔄 Updated {fresh_currency.code}")
257
+
258
+ # Progress indicator
259
+ if (i + 1) % 50 == 0:
260
+ self.stdout.write(f" Progress: {i + 1}/{len(fresh_currencies)} currencies processed")
261
+
262
+ except Exception as e:
263
+ error_msg = f"Failed to update {fresh_currency.code}: {str(e)}"
264
+ errors.append(error_msg)
265
+ logger.error(error_msg)
266
+
267
+ # Continue with other currencies unless it's a critical error
268
+ if len(errors) > 10: # Too many errors
269
+ raise CommandError(f"Too many errors ({len(errors)}), aborting")
270
+
271
+ # Summary
272
+ total_processed = created_count + updated_count
273
+ self.stdout.write(f"\n📊 Update Summary:")
274
+ self.stdout.write(f" ✅ Successfully processed: {total_processed} currencies")
275
+ self.stdout.write(f" ➕ Created new: {created_count}")
276
+ self.stdout.write(f" 🔄 Updated existing: {updated_count}")
277
+
278
+ if errors:
279
+ self.stdout.write(f" ⚠️ Errors: {len(errors)}")
280
+ for error in errors[:5]: # Show first 5 errors
281
+ self.stdout.write(f" • {error}")
282
+ if len(errors) > 5:
283
+ self.stdout.write(f" ... and {len(errors) - 5} more errors")
284
+
285
+ # Deactivate currencies not in fresh data (optional)
286
+ fresh_codes = {curr.code for curr in fresh_currencies}
287
+ stale_currencies = Currency.objects.filter(is_active=True).exclude(
288
+ code__in=fresh_codes
289
+ )
290
+
291
+ if stale_currencies.exists():
292
+ self.stdout.write(f" 📋 Found {stale_currencies.count()} currencies not in fresh data")
293
+ # Optionally deactivate them
294
+ # stale_currencies.update(is_active=False)
295
+
296
+ self.stdout.write(
297
+ self.style.SUCCESS('✅ Currency database update completed successfully!')
298
+ )
299
+
300
+ except Exception as e:
301
+ self.stdout.write(
302
+ self.style.ERROR(f'❌ Update failed and rolled back: {str(e)}')
303
+ )
304
+ raise
305
+
306
+ def _show_statistics(self):
307
+ """Show current currency statistics."""
308
+ total = Currency.objects.count()
309
+ fiat_count = Currency.objects.filter(currency_type=Currency.CurrencyType.FIAT).count()
310
+ crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
311
+ active_count = Currency.objects.filter(is_active=True).count()
312
+
313
+ # Recent updates
314
+ recent_threshold = timezone.now() - timedelta(hours=24)
315
+ recent_updates = Currency.objects.filter(rate_updated_at__gte=recent_threshold).count()
316
+
317
+ self.stdout.write(f"\n📊 Current Database Statistics:")
318
+ self.stdout.write(f" • Total currencies: {total}")
319
+ self.stdout.write(f" • Fiat currencies: {fiat_count}")
320
+ self.stdout.write(f" • Cryptocurrencies: {crypto_count}")
321
+ self.stdout.write(f" • Active currencies: {active_count}")
322
+ self.stdout.write(f" • Updated in last 24h: {recent_updates}")
323
+
324
+ # Show some examples
325
+ recent_currencies = Currency.objects.filter(
326
+ rate_updated_at__gte=recent_threshold
327
+ ).order_by('-rate_updated_at')[:5]
328
+
329
+ if recent_currencies:
330
+ self.stdout.write(f"\n🕒 Recently updated currencies:")
331
+ for currency in recent_currencies:
332
+ age = timezone.now() - currency.rate_updated_at
333
+ hours_ago = int(age.total_seconds() / 3600)
334
+ self.stdout.write(
335
+ f" • {currency.code}: ${currency.usd_rate:.6f} ({hours_ago}h ago)"
336
+ )
@@ -1,32 +1,83 @@
1
1
  """
2
- Currency managers.
2
+ Manager for Currency model.
3
3
  """
4
4
 
5
5
  from django.db import models
6
+ from django.utils import timezone
7
+ from datetime import timedelta
8
+ from typing import List, Optional
6
9
 
7
10
 
8
11
  class CurrencyManager(models.Manager):
9
- """Manager for Currency model."""
12
+ """Manager for Currency model with convenient query methods."""
10
13
 
11
- def get_active_currencies(self):
12
- """Get active currencies."""
14
+ def active(self):
15
+ """Get only active currencies."""
13
16
  return self.filter(is_active=True)
14
17
 
15
- def get_fiat_currencies(self):
16
- """Get fiat currencies."""
18
+ def fiat(self):
19
+ """Get only fiat currencies."""
20
+ return self.filter(currency_type='fiat')
21
+
22
+ def crypto(self):
23
+ """Get only cryptocurrencies."""
24
+ return self.filter(currency_type='crypto')
25
+
26
+ def active_fiat(self):
27
+ """Get active fiat currencies."""
17
28
  return self.filter(currency_type='fiat', is_active=True)
18
29
 
19
- def get_crypto_currencies(self):
20
- """Get cryptocurrencies."""
30
+ def active_crypto(self):
31
+ """Get active cryptocurrencies."""
21
32
  return self.filter(currency_type='crypto', is_active=True)
33
+
34
+ def by_code(self, code: str):
35
+ """Get currency by code (case insensitive)."""
36
+ return self.filter(code__iexact=code).first()
37
+
38
+ def supported_for_payments(self, min_amount: float = None):
39
+ """Get currencies supported for payments."""
40
+ queryset = self.active()
41
+ if min_amount:
42
+ queryset = queryset.filter(min_payment_amount__lte=min_amount)
43
+ return queryset
44
+
45
+ def recently_updated(self, hours: int = 24):
46
+ """Get currencies updated within the last N hours."""
47
+ threshold = timezone.now() - timedelta(hours=hours)
48
+ return self.filter(rate_updated_at__gte=threshold)
49
+
50
+ def outdated(self, days: int = 7):
51
+ """Get currencies with outdated rates."""
52
+ threshold = timezone.now() - timedelta(days=days)
53
+ return self.filter(
54
+ models.Q(rate_updated_at__lt=threshold) |
55
+ models.Q(rate_updated_at__isnull=True)
56
+ )
57
+
58
+ def top_crypto_by_value(self, limit: int = 10):
59
+ """Get top cryptocurrencies by USD value."""
60
+ return self.active_crypto().order_by('-usd_rate')[:limit]
61
+
62
+ def search(self, query: str):
63
+ """Search currencies by code or name."""
64
+ return self.filter(
65
+ models.Q(code__icontains=query) |
66
+ models.Q(name__icontains=query)
67
+ )
22
68
 
23
69
 
24
70
  class CurrencyNetworkManager(models.Manager):
25
71
  """Manager for CurrencyNetwork model."""
26
72
 
27
- def get_active_networks(self, currency=None):
28
- """Get active networks."""
29
- queryset = self.filter(is_active=True)
30
- if currency:
31
- queryset = queryset.filter(currency=currency)
32
- return queryset
73
+ def active(self):
74
+ """Get only active networks."""
75
+ return self.filter(is_active=True)
76
+
77
+ def for_currency(self, currency_code: str):
78
+ """Get networks for a specific currency."""
79
+ return self.filter(currency__code__iexact=currency_code)
80
+
81
+ def active_for_currency(self, currency_code: str):
82
+ """Get active networks for a specific currency."""
83
+ return self.active().filter(currency__code__iexact=currency_code)
@@ -11,6 +11,7 @@ from django.conf import settings
11
11
  from django.utils import timezone
12
12
  from ..models import APIKey, Subscription, EndpointGroup
13
13
  from ..services import ApiKeyCache, RateLimitCache
14
+ from ..services.security import error_handler, SecurityError
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -58,6 +59,15 @@ class APIAccessMiddleware(MiddlewareMixin):
58
59
  # Extract API key
59
60
  api_key = self._extract_api_key(request)
60
61
  if not api_key:
62
+ security_error = SecurityError(
63
+ "API key required for protected endpoint",
64
+ details={'path': request.path, 'method': request.method}
65
+ )
66
+ error_handler.handle_error(security_error, {
67
+ 'middleware': 'api_access',
68
+ 'operation': 'api_key_extraction'
69
+ }, request)
70
+
61
71
  return self._error_response(
62
72
  'API key required',
63
73
  status=401,
@@ -67,6 +77,20 @@ class APIAccessMiddleware(MiddlewareMixin):
67
77
  # Validate API key
68
78
  api_key_obj = self._validate_api_key(api_key)
69
79
  if not api_key_obj:
80
+ security_error = SecurityError(
81
+ f"Invalid or expired API key attempted",
82
+ details={
83
+ 'api_key_prefix': api_key[:8] + '...' if len(api_key) > 8 else api_key,
84
+ 'path': request.path,
85
+ 'method': request.method,
86
+ 'ip_address': self._get_client_ip(request)
87
+ }
88
+ )
89
+ error_handler.handle_error(security_error, {
90
+ 'middleware': 'api_access',
91
+ 'operation': 'api_key_validation'
92
+ }, request)
93
+
70
94
  return self._error_response(
71
95
  'Invalid or expired API key',
72
96
  status=401,
@@ -249,6 +273,15 @@ class APIAccessMiddleware(MiddlewareMixin):
249
273
  except Exception as e:
250
274
  logger.error(f"Error tracking usage: {e}")
251
275
 
276
+ def _get_client_ip(self, request: HttpRequest) -> str:
277
+ """Extract client IP address from request."""
278
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
279
+ if x_forwarded_for:
280
+ ip = x_forwarded_for.split(',')[0].strip()
281
+ else:
282
+ ip = request.META.get('REMOTE_ADDR', '')
283
+ return ip
284
+
252
285
  def _error_response(self, message: str, status: int = 400, error_code: str = 'ERROR') -> JsonResponse:
253
286
  """Return standardized error response."""
254
287
 
@@ -1,4 +1,4 @@
1
- # Generated by Django 5.2.6 on 2025-09-23 14:27
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
@@ -498,6 +498,8 @@ class Migration(migrations.Migration):
498
498
  models.CharField(
499
499
  choices=[
500
500
  ("nowpayments", "NowPayments"),
501
+ ("cryptapi", "CryptAPI"),
502
+ ("cryptomus", "Cryptomus"),
501
503
  ("stripe", "Stripe"),
502
504
  ("internal", "Internal"),
503
505
  ],
@@ -577,6 +579,76 @@ class Migration(migrations.Migration):
577
579
  blank=True, help_text="Raw webhook data from provider", null=True
578
580
  ),
579
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
+ ),
580
652
  (
581
653
  "expires_at",
582
654
  models.DateTimeField(
@@ -964,6 +1036,27 @@ class Migration(migrations.Migration):
964
1036
  model_name="universalpayment",
965
1037
  index=models.Index(fields=["processed_at"], name="universal_p_process_1c8a1f_idx"),
966
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
+ ),
967
1060
  migrations.AddIndex(
968
1061
  model_name="transaction",
969
1062
  index=models.Index(