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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/payments/admin_interface/old/payments/base.html +175 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
- django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
- django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
- django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
- django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
- django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
- django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
- django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
- django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
- django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
- django_cfg/apps/payments/admin_interface/views/base.py +114 -0
- django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
- django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
- django_cfg/apps/payments/config/helpers.py +2 -2
- django_cfg/apps/payments/management/commands/cleanup_expired_data.py +16 -6
- django_cfg/apps/payments/management/commands/currency_stats.py +72 -5
- django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
- django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
- django_cfg/apps/payments/middleware/api_access.py +35 -34
- django_cfg/apps/payments/migrations/0001_initial.py +1 -1
- django_cfg/apps/payments/models/managers/api_key_managers.py +4 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
- django_cfg/apps/payments/models/subscriptions.py +0 -24
- django_cfg/apps/payments/services/cache/__init__.py +1 -1
- django_cfg/apps/payments/services/core/balance_service.py +13 -2
- django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
- django_cfg/apps/payments/services/providers/registry.py +20 -0
- django_cfg/apps/payments/signals/balance_signals.py +7 -4
- django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
- django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
- django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
- django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
- django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
- django_cfg/apps/payments/urls.py +4 -0
- django_cfg/apps/payments/urls_admin.py +37 -18
- django_cfg/apps/payments/views/api/api_keys.py +14 -0
- django_cfg/apps/payments/views/api/base.py +1 -0
- django_cfg/apps/payments/views/api/currencies.py +2 -2
- django_cfg/apps/payments/views/api/payments.py +11 -5
- django_cfg/apps/payments/views/api/subscriptions.py +36 -31
- django_cfg/apps/payments/views/overview/__init__.py +40 -0
- django_cfg/apps/payments/views/overview/serializers.py +205 -0
- django_cfg/apps/payments/views/overview/services.py +439 -0
- django_cfg/apps/payments/views/overview/urls.py +27 -0
- django_cfg/apps/payments/views/overview/views.py +231 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
- django_cfg/apps/payments/views/serializers/balances.py +5 -8
- django_cfg/apps/payments/views/serializers/currencies.py +2 -6
- django_cfg/apps/payments/views/serializers/payments.py +37 -32
- django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
- django_cfg/apps/urls.py +2 -1
- django_cfg/core/config.py +25 -15
- django_cfg/core/generation.py +12 -12
- django_cfg/core/integration/display/startup.py +1 -1
- django_cfg/core/validation.py +4 -4
- django_cfg/management/commands/show_config.py +2 -2
- django_cfg/management/commands/tree.py +1 -3
- django_cfg/middleware/__init__.py +2 -0
- django_cfg/middleware/static_nocache.py +55 -0
- django_cfg/models/payments.py +13 -15
- django_cfg/models/security.py +15 -0
- django_cfg/modules/django_ngrok.py +6 -0
- django_cfg/modules/django_unfold/dashboard.py +1 -3
- django_cfg/utils/smart_defaults.py +41 -1
- {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
- {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/RECORD +98 -65
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
- django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
- {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.3.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
- {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(
|
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
|
-
|
133
|
-
|
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
|
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
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
296
|
+
provider='system', # System-level rate
|
304
297
|
provider_currency_code=currency.code,
|
305
298
|
defaults={
|
306
|
-
'
|
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
|
-
|
315
|
-
provider_currency.
|
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
|
356
|
+
# Count currencies with provider configs (simplified since rate fields don't exist)
|
368
357
|
currencies_with_rates = Currency.objects.filter(
|
369
|
-
|
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('
|
310
|
+
provider_stats = ProviderCurrency.objects.values('provider').annotate(
|
311
311
|
total=Count('id'),
|
312
|
-
enabled=Count('id', filter=
|
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['
|
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
|
-
|
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.
|
48
|
-
self.
|
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.
|
67
|
-
|
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
|
73
|
-
self.
|
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
|
-
'
|
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
|
99
|
-
if not self.
|
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
|
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
|
-
|
191
|
-
|
192
|
-
|
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
|
195
|
-
for pattern in self.
|
194
|
+
# Check protected patterns
|
195
|
+
for pattern in self.protected_patterns:
|
196
196
|
if pattern.match(path):
|
197
|
-
return
|
197
|
+
return True
|
198
198
|
|
199
|
-
|
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
|
-
).
|
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
|
-
'
|
323
|
-
'requests_remaining': subscription
|
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
|
|
@@ -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
|
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
|
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
|
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
|
42
|
+
"""Check if ngrok tunnel is actually active."""
|
43
43
|
try:
|
44
|
-
from django_cfg.modules.django_ngrok import
|
45
|
-
return
|
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
|
-
|
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
|
-
|
48
|
-
|
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
|
)
|