django-cfg 1.3.3__py3-none-any.whl → 1.3.5__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 (101) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin_interface/old/payments/base.html +175 -0
  3. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
  4. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
  5. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
  6. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
  7. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
  8. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
  9. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
  10. django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
  11. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
  12. django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
  13. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
  14. django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
  23. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
  24. django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
  25. django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
  26. django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
  27. django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
  28. django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
  29. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
  30. django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
  31. django_cfg/apps/payments/admin_interface/views/base.py +114 -0
  32. django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
  33. django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
  34. django_cfg/apps/payments/config/helpers.py +2 -2
  35. django_cfg/apps/payments/management/commands/cleanup_expired_data.py +16 -6
  36. django_cfg/apps/payments/management/commands/currency_stats.py +72 -5
  37. django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
  38. django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
  39. django_cfg/apps/payments/middleware/api_access.py +35 -34
  40. django_cfg/apps/payments/migrations/0001_initial.py +1 -1
  41. django_cfg/apps/payments/models/managers/api_key_managers.py +4 -0
  42. django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
  43. django_cfg/apps/payments/models/subscriptions.py +0 -24
  44. django_cfg/apps/payments/services/cache/__init__.py +1 -1
  45. django_cfg/apps/payments/services/core/balance_service.py +13 -2
  46. django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
  47. django_cfg/apps/payments/services/providers/registry.py +20 -0
  48. django_cfg/apps/payments/signals/balance_signals.py +7 -4
  49. django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
  50. django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
  51. django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
  52. django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
  53. django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
  54. django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
  55. django_cfg/apps/payments/urls.py +4 -0
  56. django_cfg/apps/payments/urls_admin.py +37 -18
  57. django_cfg/apps/payments/views/api/api_keys.py +14 -0
  58. django_cfg/apps/payments/views/api/base.py +1 -0
  59. django_cfg/apps/payments/views/api/currencies.py +2 -2
  60. django_cfg/apps/payments/views/api/payments.py +11 -5
  61. django_cfg/apps/payments/views/api/subscriptions.py +36 -31
  62. django_cfg/apps/payments/views/overview/__init__.py +40 -0
  63. django_cfg/apps/payments/views/overview/serializers.py +205 -0
  64. django_cfg/apps/payments/views/overview/services.py +439 -0
  65. django_cfg/apps/payments/views/overview/urls.py +27 -0
  66. django_cfg/apps/payments/views/overview/views.py +231 -0
  67. django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
  68. django_cfg/apps/payments/views/serializers/balances.py +5 -8
  69. django_cfg/apps/payments/views/serializers/currencies.py +2 -6
  70. django_cfg/apps/payments/views/serializers/payments.py +37 -32
  71. django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
  72. django_cfg/apps/urls.py +2 -1
  73. django_cfg/core/config.py +25 -15
  74. django_cfg/core/generation.py +12 -12
  75. django_cfg/core/integration/display/startup.py +1 -1
  76. django_cfg/core/validation.py +4 -4
  77. django_cfg/management/commands/show_config.py +2 -2
  78. django_cfg/management/commands/tree.py +1 -3
  79. django_cfg/middleware/__init__.py +2 -0
  80. django_cfg/middleware/static_nocache.py +55 -0
  81. django_cfg/models/payments.py +13 -15
  82. django_cfg/models/security.py +15 -0
  83. django_cfg/modules/django_ngrok.py +6 -0
  84. django_cfg/modules/django_unfold/dashboard.py +1 -3
  85. django_cfg/utils/smart_defaults.py +41 -1
  86. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
  87. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/RECORD +98 -65
  88. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
  89. django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
  90. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
  91. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
  92. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
  93. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
  94. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
  95. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
  96. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
  97. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
  98. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
  99. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
  100. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
  101. {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/licenses/LICENSE +0 -0
@@ -73,6 +73,26 @@ class Command(BaseCommand):
73
73
  default='table',
74
74
  help='Output format (default: table)'
75
75
  )
76
+
77
+ # Additional arguments expected by tests
78
+ parser.add_argument(
79
+ '--days',
80
+ type=int,
81
+ default=30,
82
+ help='Filter data for last N days (default: 30)'
83
+ )
84
+
85
+ parser.add_argument(
86
+ '--currency',
87
+ type=str,
88
+ help='Show statistics for specific currency code'
89
+ )
90
+
91
+ parser.add_argument(
92
+ '--verbose',
93
+ action='store_true',
94
+ help='Show verbose output (same as --detailed)'
95
+ )
76
96
 
77
97
  def handle(self, *args, **options):
78
98
  """Execute the command."""
@@ -80,10 +100,16 @@ class Command(BaseCommand):
80
100
  self.options = options
81
101
  self.show_header()
82
102
 
103
+ # Handle --verbose as alias for --detailed
104
+ if options['verbose']:
105
+ options['detailed'] = True
106
+
83
107
  if options['check_rates']:
84
108
  self.check_rate_freshness()
85
109
  elif options['provider']:
86
110
  self.show_provider_stats(options['provider'])
111
+ elif options['currency']:
112
+ self.show_currency_stats(options['currency'])
87
113
  else:
88
114
  self.show_general_stats()
89
115
 
@@ -127,11 +153,16 @@ class Command(BaseCommand):
127
153
 
128
154
  # Provider currency stats
129
155
  total_provider_currencies = ProviderCurrency.objects.count()
130
- active_provider_currencies = ProviderCurrency.objects.filter(is_active=True).count()
156
+ active_provider_currencies = ProviderCurrency.objects.filter(is_enabled=True).count()
157
+
158
+ # Payment stats (filtered by days if specified)
159
+ payment_filter = Q()
160
+ if self.options.get('days'):
161
+ days_ago = timezone.now() - timedelta(days=self.options['days'])
162
+ payment_filter = Q(created_at__gte=days_ago)
131
163
 
132
- # Payment stats
133
- total_payments = UniversalPayment.objects.count()
134
- completed_payments = UniversalPayment.objects.filter(status='completed').count()
164
+ total_payments = UniversalPayment.objects.filter(payment_filter).count()
165
+ completed_payments = UniversalPayment.objects.filter(payment_filter, status='completed').count()
135
166
 
136
167
  self.stdout.write(self.style.SUCCESS("📈 GENERAL STATISTICS"))
137
168
  self.stdout.write("-" * 40)
@@ -235,6 +266,42 @@ class Command(BaseCommand):
235
266
 
236
267
  self.stdout.write("")
237
268
 
269
+ def show_currency_stats(self, currency_code: str):
270
+ """Show statistics for specific currency."""
271
+ self.stdout.write(self.style.SUCCESS(f"💰 CURRENCY STATISTICS: {currency_code.upper()}"))
272
+ self.stdout.write("-" * 40)
273
+
274
+ try:
275
+ currency = Currency.objects.get(code=currency_code.upper())
276
+ except Currency.DoesNotExist:
277
+ self.stdout.write(self.style.ERROR(f"Currency {currency_code} not found"))
278
+ return
279
+
280
+ # Basic currency info
281
+ self.stdout.write(f"Name: {currency.name}")
282
+ self.stdout.write(f"Type: {currency.currency_type}")
283
+ self.stdout.write(f"Active: {'Yes' if currency.is_active else 'No'}")
284
+ self.stdout.write(f"Created: {currency.created_at.strftime('%Y-%m-%d')}")
285
+ self.stdout.write(f"Updated: {currency.updated_at.strftime('%Y-%m-%d')}")
286
+
287
+ # Payment statistics
288
+ payments = UniversalPayment.objects.filter(currency=currency)
289
+ completed_payments = payments.filter(status='completed')
290
+
291
+ if payments.exists():
292
+ total_volume = completed_payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
293
+ avg_payment = completed_payments.aggregate(Avg('amount_usd'))['amount_usd__avg'] or 0
294
+
295
+ self.stdout.write(f"\nPayment Statistics:")
296
+ self.stdout.write(f" Total Payments: {payments.count()}")
297
+ self.stdout.write(f" Completed: {completed_payments.count()}")
298
+ self.stdout.write(f" Total Volume: ${intcomma(total_volume)}")
299
+ self.stdout.write(f" Average Payment: ${intcomma(f'{avg_payment:.2f}')}")
300
+ else:
301
+ self.stdout.write(f"\nNo payments found for {currency_code}")
302
+
303
+ self.stdout.write("")
304
+
238
305
  def check_rate_freshness(self):
239
306
  """Check for outdated exchange rates."""
240
307
  self.stdout.write(self.style.SUCCESS("🕐 RATE FRESHNESS CHECK"))
@@ -302,7 +369,7 @@ class Command(BaseCommand):
302
369
  ("Total Payments", provider_payments.count()),
303
370
  ("Completed Payments", completed_payments.count()),
304
371
  ("Total Volume", f"${intcomma(total_volume)}"),
305
- ("Average Payment", f"${intcomma(avg_payment):.2f}"),
372
+ ("Average Payment", f"${intcomma(f'{avg_payment:.2f}')}"),
306
373
  ]
307
374
 
308
375
  for label, value in stats:
@@ -262,16 +262,9 @@ class Command(BaseCommand):
262
262
 
263
263
  # Filter by staleness unless forced
264
264
  if not options['force']:
265
- stale_threshold = timezone.now() - timedelta(hours=12)
266
-
267
- # Get currencies that need rate updates through ProviderCurrency
268
- currencies_needing_update = Currency.objects.filter(
269
- Q(providercurrency__usd_rate__isnull=True) |
270
- Q(providercurrency__rate_updated_at__isnull=True) |
271
- Q(providercurrency__rate_updated_at__lt=stale_threshold)
272
- ).distinct()
273
-
274
- queryset = queryset.filter(id__in=currencies_needing_update)
265
+ # For now, skip staleness check since rate fields don't exist
266
+ # TODO: Implement proper rate tracking fields
267
+ pass
275
268
 
276
269
  # Apply limit
277
270
  queryset = queryset[:options['limit']]
@@ -300,20 +293,16 @@ class Command(BaseCommand):
300
293
  # Update rate in ProviderCurrency (create if doesn't exist)
301
294
  provider_currency, created = ProviderCurrency.objects.get_or_create(
302
295
  currency=currency,
303
- provider_name='system', # System-level rate
296
+ provider='system', # System-level rate
304
297
  provider_currency_code=currency.code,
305
298
  defaults={
306
- 'usd_rate': usd_rate,
307
- 'rate_updated_at': timezone.now(),
308
- 'is_enabled': True,
309
- 'is_stable': currency.currency_type == Currency.CurrencyType.FIAT
299
+ 'is_enabled': True
310
300
  }
311
301
  )
312
302
 
313
303
  if not created:
314
- provider_currency.usd_rate = usd_rate
315
- provider_currency.rate_updated_at = timezone.now()
316
- provider_currency.save(update_fields=['usd_rate', 'rate_updated_at'])
304
+ # TODO: Add rate tracking fields to ProviderCurrency model
305
+ provider_currency.save() # Touch the record to update timestamp
317
306
 
318
307
  # Update currency's exchange rate source
319
308
  currency.exchange_rate_source = 'django_currency'
@@ -364,9 +353,9 @@ class Command(BaseCommand):
364
353
  crypto_count = Currency.objects.filter(currency_type=Currency.CurrencyType.CRYPTO).count()
365
354
  active_count = Currency.objects.filter(is_active=True).count()
366
355
 
367
- # Count currencies with rates
356
+ # Count currencies with provider configs (simplified since rate fields don't exist)
368
357
  currencies_with_rates = Currency.objects.filter(
369
- providercurrency__usd_rate__isnull=False
358
+ provider_configs__isnull=False
370
359
  ).distinct().count()
371
360
 
372
361
  rate_coverage = (currencies_with_rates / total_currencies * 100) if total_currencies > 0 else 0
@@ -305,18 +305,18 @@ class Command(BaseCommand):
305
305
  self.stdout.write(f" Enabled: {enabled_provider_currencies}")
306
306
 
307
307
  # Stats by provider
308
- from django.db.models import Count
308
+ from django.db.models import Count, Q
309
309
 
310
- provider_stats = ProviderCurrency.objects.values('provider_name').annotate(
310
+ provider_stats = ProviderCurrency.objects.values('provider').annotate(
311
311
  total=Count('id'),
312
- enabled=Count('id', filter=models.Q(is_enabled=True))
312
+ enabled=Count('id', filter=Q(is_enabled=True))
313
313
  ).order_by('-total')
314
314
 
315
315
  if provider_stats:
316
316
  self.stdout.write(f"\n📈 By Provider:")
317
317
  for stat in provider_stats:
318
318
  self.stdout.write(
319
- f" - {stat['provider_name']}: {stat['total']} total, {stat['enabled']} enabled"
319
+ f" - {stat['provider']}: {stat['total']} total, {stat['enabled']} enabled"
320
320
  )
321
321
 
322
322
  # Recent activity
@@ -330,7 +330,7 @@ class Command(BaseCommand):
330
330
 
331
331
  # Rate coverage
332
332
  currencies_with_rates = ProviderCurrency.objects.filter(
333
- usd_rate__isnull=False
333
+ is_enabled=True
334
334
  ).count()
335
335
 
336
336
  rate_coverage = (currencies_with_rates / total_provider_currencies * 100) if total_provider_currencies > 0 else 0
@@ -44,8 +44,8 @@ class APIAccessMiddleware(MiddlewareMixin):
44
44
 
45
45
  # Configuration from django-cfg
46
46
  self.enabled = middleware_config['enabled']
47
- self.api_prefixes = middleware_config['api_prefixes']
48
- self.exempt_paths = middleware_config['exempt_paths']
47
+ self.protected_paths = middleware_config.get('protected_paths', [])
48
+ self.protected_patterns_raw = middleware_config.get('protected_patterns', [])
49
49
  self.cache_timeout = middleware_config['cache_timeouts']['api_key']
50
50
 
51
51
  # Default settings (can be overridden by Constance)
@@ -61,29 +61,32 @@ class APIAccessMiddleware(MiddlewareMixin):
61
61
 
62
62
  except Exception as e:
63
63
  logger.warning(f"Failed to load middleware config, using defaults: {e}")
64
- # Fallback defaults
64
+ # Fallback defaults - whitelist approach
65
65
  self.enabled = True
66
- self.api_prefixes = ['/api/']
67
- self.exempt_paths = ['/api/health/', '/admin/']
66
+ self.protected_paths = [
67
+ '/api/admin/', # Admin API endpoints
68
+ '/api/private/', # Private API endpoints
69
+ '/api/secure/', # Secure API endpoints
70
+ ]
71
+ self.protected_patterns_raw = [
72
+ r'^/api/admin/.*$', # All admin API endpoints
73
+ r'^/api/private/.*$', # All private API endpoints
74
+ r'^/api/secure/.*$', # All secure API endpoints
75
+ ]
68
76
  self.cache_timeout = 300
69
77
  self.strict_mode = False
70
78
  self.require_api_key = True
71
79
 
72
- # Compile exempt path patterns (static for now)
73
- self.exempt_patterns = [
74
- re.compile(pattern) for pattern in [
75
- r'^/api/payments/[^/]+/status/$',
76
- r'^/api/webhooks/[^/]+/$',
77
- r'^/api/payments/webhooks/(providers|health|stats)/$', # Admin webhook endpoints
78
- r'^/api/currencies/(rates|supported|convert)/$',
79
- ]
80
+ # Compile protected path patterns
81
+ self.protected_patterns = [
82
+ re.compile(pattern) for pattern in self.protected_patterns_raw
80
83
  ]
81
84
 
82
85
  logger.info(f"API Access Middleware initialized", extra={
83
86
  'enabled': self.enabled,
84
87
  'strict_mode': self.strict_mode,
85
88
  'require_api_key': self.require_api_key,
86
- 'api_prefixes': self.api_prefixes
89
+ 'protected_paths': self.protected_paths
87
90
  })
88
91
 
89
92
  def process_request(self, request: HttpRequest) -> Optional[JsonResponse]:
@@ -95,8 +98,8 @@ class APIAccessMiddleware(MiddlewareMixin):
95
98
  if not self.enabled:
96
99
  return None
97
100
 
98
- # Check if this path requires authentication
99
- if not self._requires_authentication(request.path):
101
+ # Check if this path is protected (whitelist approach)
102
+ if not self._is_protected_path(request.path):
100
103
  return None
101
104
 
102
105
  # Start timing for performance monitoring
@@ -177,26 +180,24 @@ class APIAccessMiddleware(MiddlewareMixin):
177
180
  # Graceful degradation: allow access but log the issue
178
181
  return None
179
182
 
180
- def _requires_authentication(self, path: str) -> bool:
183
+ def _is_protected_path(self, path: str) -> bool:
181
184
  """
182
- Check if the given path requires API authentication.
183
- """
184
- # Check if path starts with API prefix
185
- requires_auth = any(path.startswith(prefix) for prefix in self.api_prefixes)
186
-
187
- if not requires_auth:
188
- return False
185
+ Check if the given path is protected and requires API authentication.
189
186
 
190
- # Check exempt paths
191
- if path in self.exempt_paths:
192
- return False
187
+ Whitelist approach: only paths explicitly listed as protected require API key.
188
+ """
189
+ # Check exact protected paths
190
+ for protected_path in self.protected_paths:
191
+ if path.startswith(protected_path):
192
+ return True
193
193
 
194
- # Check exempt patterns
195
- for pattern in self.exempt_patterns:
194
+ # Check protected patterns
195
+ for pattern in self.protected_patterns:
196
196
  if pattern.match(path):
197
- return False
197
+ return True
198
198
 
199
- return True
199
+ # Path is not protected - no API key required
200
+ return False
200
201
 
201
202
  def _extract_api_key(self, request: HttpRequest) -> Optional[str]:
202
203
  """
@@ -300,7 +301,7 @@ class APIAccessMiddleware(MiddlewareMixin):
300
301
  user=api_key.user,
301
302
  status=Subscription.SubscriptionStatus.ACTIVE,
302
303
  expires_at__gt=timezone.now()
303
- ).select_related('tariff', 'endpoint_group')
304
+ ).prefetch_related('endpoint_groups')
304
305
 
305
306
  if not active_subscriptions.exists():
306
307
  return {
@@ -319,8 +320,8 @@ class APIAccessMiddleware(MiddlewareMixin):
319
320
  'allowed': True,
320
321
  'subscription_id': str(subscription.id),
321
322
  'tier': subscription.tier,
322
- 'tariff_name': subscription.tariff.name if subscription.tariff else None,
323
- 'requests_remaining': subscription.requests_remaining(),
323
+ 'tier_name': subscription.tier,
324
+ 'requests_remaining': 'unlimited', # TODO: Implement rate limiting per subscription
324
325
  'expires_at': subscription.expires_at.isoformat() if subscription.expires_at else None
325
326
  }
326
327
 
@@ -1,4 +1,4 @@
1
- # Generated by Django 5.2.6 on 2025-09-26 05:27
1
+ # Generated by Django 5.2.6 on 2025-09-27 10:37
2
2
 
3
3
  import django.core.validators
4
4
  import django.db.models.deletion
@@ -93,6 +93,10 @@ class APIKeyManager(models.Manager):
93
93
  """Get API keys expiring soon."""
94
94
  return self.get_queryset().expiring_soon(days)
95
95
 
96
+ def by_user(self, user):
97
+ """Get API keys by user."""
98
+ return self.get_queryset().by_user(user)
99
+
96
100
  # Business logic methods
97
101
  def increment_api_key_usage(self, api_key_id, ip_address=None):
98
102
  """
@@ -244,6 +244,11 @@ class PaymentManager(models.Manager):
244
244
  """Get active payments."""
245
245
  return self.get_queryset().active()
246
246
 
247
+ # User-based methods
248
+ def by_user(self, user):
249
+ """Get payments by user."""
250
+ return self.get_queryset().by_user(user)
251
+
247
252
  # Provider-based methods
248
253
  def by_provider(self, provider):
249
254
  """Get payments by provider."""
@@ -102,30 +102,6 @@ class SubscriptionQuerySet(models.QuerySet):
102
102
  return self.filter(user=user)
103
103
 
104
104
 
105
- class SubscriptionManager(models.Manager):
106
- """Manager for subscription operations."""
107
-
108
- def get_queryset(self):
109
- """Return optimized queryset by default."""
110
- return SubscriptionQuerySet(self.model, using=self._db)
111
-
112
- def optimized(self):
113
- """Get optimized queryset."""
114
- return self.get_queryset().optimized()
115
-
116
- def active(self):
117
- """Get active subscriptions."""
118
- return self.get_queryset().active()
119
-
120
- def expired(self):
121
- """Get expired subscriptions."""
122
- return self.get_queryset().expired()
123
-
124
- def by_tier(self, tier):
125
- """Get subscriptions by tier."""
126
- return self.get_queryset().by_tier(tier)
127
-
128
-
129
105
  class Subscription(UUIDTimestampedModel):
130
106
  """
131
107
  User subscription model for API access control.
@@ -4,7 +4,7 @@ Cache services for the Universal Payment System v2.0.
4
4
  Redis-backed caching with type safety and automatic key management.
5
5
  """
6
6
 
7
- from .cache_service import CacheService, get_cache_service, SimpleCache, ApiKeyCache, RateLimitCache
7
+ from ..cache_service import CacheService, get_cache_service, SimpleCache, ApiKeyCache, RateLimitCache
8
8
 
9
9
  __all__ = [
10
10
  'CacheService',
@@ -5,10 +5,12 @@ Handles user balance operations and transaction management.
5
5
  """
6
6
 
7
7
  from typing import Optional, Dict, Any, List
8
- from django.contrib.auth.models import User
8
+ from django.contrib.auth import get_user_model
9
9
  from django.db import models
10
10
  from django.utils import timezone
11
11
 
12
+ User = get_user_model()
13
+
12
14
  from .base import BaseService
13
15
  from ..types import (
14
16
  BalanceUpdateRequest, BalanceResult, TransactionData,
@@ -141,7 +143,16 @@ class BalanceService(BaseService):
141
143
 
142
144
  # Convert to response data
143
145
  balance_data = BalanceData.model_validate(balance)
144
- transaction_data = TransactionData.model_validate(transaction)
146
+ transaction_data = TransactionData(
147
+ id=str(transaction.id),
148
+ user_id=transaction.user_id,
149
+ amount=float(transaction.amount_usd),
150
+ transaction_type=transaction.transaction_type,
151
+ description=transaction.description,
152
+ payment_id=transaction.payment_id,
153
+ metadata=transaction.metadata or {},
154
+ created_at=transaction.created_at
155
+ )
145
156
 
146
157
  self._log_operation(
147
158
  "update_balance",
@@ -39,9 +39,9 @@ def get_api_base_url() -> str:
39
39
 
40
40
 
41
41
  def is_ngrok_available() -> bool:
42
- """Check if ngrok tunnel is available."""
42
+ """Check if ngrok tunnel is actually active."""
43
43
  try:
44
- from django_cfg.modules.django_ngrok import get_tunnel_url
45
- return get_tunnel_url() is not None
44
+ from django_cfg.modules.django_ngrok import is_tunnel_active
45
+ return is_tunnel_active()
46
46
  except ImportError:
47
47
  return False
@@ -411,6 +411,26 @@ class ProviderRegistry:
411
411
  self._provider_configs[provider_name] = config_class
412
412
  logger.info(f"Registered provider class: {provider_name}")
413
413
 
414
+ def register_provider(self, provider_name: str, provider_instance: BaseProvider):
415
+ """
416
+ Register provider instance directly (for testing).
417
+
418
+ Args:
419
+ provider_name: Provider name
420
+ provider_instance: Provider instance
421
+ """
422
+ self._providers[provider_name] = provider_instance
423
+ logger.info(f"Registered provider instance: {provider_name}")
424
+
425
+ @property
426
+ def providers(self) -> Dict[str, BaseProvider]:
427
+ """Get dictionary of registered providers."""
428
+ return self._providers.copy()
429
+
430
+ def list_providers(self) -> List[str]:
431
+ """Get list of registered provider names."""
432
+ return list(self._providers.keys())
433
+
414
434
  def __len__(self) -> int:
415
435
  """Get number of initialized providers."""
416
436
  return len(self._providers)
@@ -9,6 +9,7 @@ from django.db.models.signals import post_save, post_delete, pre_save
9
9
  from django.dispatch import receiver
10
10
  from django.core.cache import cache
11
11
  from django.utils import timezone
12
+ from decimal import Decimal
12
13
 
13
14
  from ..models import UserBalance, Transaction
14
15
  from django_cfg.modules.django_logger import get_logger
@@ -22,7 +23,8 @@ def store_original_balance(sender, instance: UserBalance, **kwargs):
22
23
  if instance.pk:
23
24
  try:
24
25
  original = UserBalance.objects.get(pk=instance.pk)
25
- instance._original_balance = original.balance_usd
26
+ # Ensure _original_balance is always Decimal
27
+ instance._original_balance = Decimal(str(original.balance_usd)) if original.balance_usd is not None else None
26
28
  except UserBalance.DoesNotExist:
27
29
  instance._original_balance = None
28
30
  else:
@@ -44,8 +46,9 @@ def handle_balance_change(sender, instance: UserBalance, created: bool, **kwargs
44
46
  else:
45
47
  # Check if balance changed
46
48
  if hasattr(instance, '_original_balance'):
47
- old_balance = instance._original_balance or 0.0
48
- new_balance = instance.balance_usd
49
+ # Ensure both values are Decimal for consistent arithmetic
50
+ old_balance = Decimal(str(instance._original_balance or 0.0))
51
+ new_balance = Decimal(str(instance.balance_usd))
49
52
 
50
53
  if old_balance != new_balance:
51
54
  balance_change = new_balance - old_balance
@@ -143,7 +146,7 @@ def _handle_zero_balance(balance: UserBalance):
143
146
  f"zero_balance:{balance.user.id}",
144
147
  {
145
148
  'timestamp': timezone.now().isoformat(),
146
- 'previous_balance': getattr(balance, '_original_balance', 0.0)
149
+ 'previous_balance': getattr(balance, '_original_balance', Decimal('0.0'))
147
150
  },
148
151
  timeout=86400 * 7 # 7 days
149
152
  )