django-cfg 1.2.22__py3-none-any.whl → 1.2.23__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/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +87 -0
- django_cfg/apps/payments/config/module.py +162 -0
- django_cfg/apps/payments/config/providers.py +93 -0
- django_cfg/apps/payments/config/settings.py +136 -0
- django_cfg/apps/payments/config/utils.py +198 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/middleware/api_access.py +261 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +32 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +2 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +58 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +449 -0
- django_cfg/apps/payments/services/core/payment_service.py +393 -0
- django_cfg/apps/payments/services/core/subscription_service.py +616 -0
- django_cfg/apps/payments/services/internal_types.py +266 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/providers/__init__.py +19 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +99 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +150 -0
- django_cfg/apps/payments/signals/payment_signals.py +127 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +42 -0
- django_cfg/apps/payments/utils/config_utils.py +243 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/models/revolution.py +1 -1
- django_cfg/modules/base.py +1 -1
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/RECORD +63 -26
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.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
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-23 14:27
|
2
2
|
|
3
3
|
import django.core.validators
|
4
4
|
import django.db.models.deletion
|
5
|
+
import uuid
|
5
6
|
from django.conf import settings
|
6
7
|
from django.db import migrations, models
|
7
8
|
|
@@ -172,8 +173,12 @@ class Migration(migrations.Migration):
|
|
172
173
|
fields=[
|
173
174
|
(
|
174
175
|
"id",
|
175
|
-
models.
|
176
|
-
|
176
|
+
models.UUIDField(
|
177
|
+
default=uuid.uuid4,
|
178
|
+
editable=False,
|
179
|
+
help_text="Unique identifier",
|
180
|
+
primary_key=True,
|
181
|
+
serialize=False,
|
177
182
|
),
|
178
183
|
),
|
179
184
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -252,8 +257,12 @@ class Migration(migrations.Migration):
|
|
252
257
|
fields=[
|
253
258
|
(
|
254
259
|
"id",
|
255
|
-
models.
|
256
|
-
|
260
|
+
models.UUIDField(
|
261
|
+
default=uuid.uuid4,
|
262
|
+
editable=False,
|
263
|
+
help_text="Unique identifier",
|
264
|
+
primary_key=True,
|
265
|
+
serialize=False,
|
257
266
|
),
|
258
267
|
),
|
259
268
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -442,8 +451,12 @@ class Migration(migrations.Migration):
|
|
442
451
|
fields=[
|
443
452
|
(
|
444
453
|
"id",
|
445
|
-
models.
|
446
|
-
|
454
|
+
models.UUIDField(
|
455
|
+
default=uuid.uuid4,
|
456
|
+
editable=False,
|
457
|
+
help_text="Unique identifier",
|
458
|
+
primary_key=True,
|
459
|
+
serialize=False,
|
447
460
|
),
|
448
461
|
),
|
449
462
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -606,8 +619,12 @@ class Migration(migrations.Migration):
|
|
606
619
|
fields=[
|
607
620
|
(
|
608
621
|
"id",
|
609
|
-
models.
|
610
|
-
|
622
|
+
models.UUIDField(
|
623
|
+
default=uuid.uuid4,
|
624
|
+
editable=False,
|
625
|
+
help_text="Unique identifier",
|
626
|
+
primary_key=True,
|
627
|
+
serialize=False,
|
611
628
|
),
|
612
629
|
),
|
613
630
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -767,8 +784,12 @@ class Migration(migrations.Migration):
|
|
767
784
|
fields=[
|
768
785
|
(
|
769
786
|
"id",
|
770
|
-
models.
|
771
|
-
|
787
|
+
models.UUIDField(
|
788
|
+
default=uuid.uuid4,
|
789
|
+
editable=False,
|
790
|
+
help_text="Unique identifier",
|
791
|
+
primary_key=True,
|
792
|
+
serialize=False,
|
772
793
|
),
|
773
794
|
),
|
774
795
|
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
|
@@ -28,6 +28,15 @@ from .api_keys import APIKey
|
|
28
28
|
# Event sourcing
|
29
29
|
from .events import PaymentEvent
|
30
30
|
|
31
|
+
# TextChoices classes for external use (accessing inner classes)
|
32
|
+
CurrencyType = Currency.CurrencyType
|
33
|
+
PaymentStatus = UniversalPayment.PaymentStatus
|
34
|
+
PaymentProvider = UniversalPayment.PaymentProvider
|
35
|
+
TransactionType = Transaction.TransactionType
|
36
|
+
SubscriptionStatus = Subscription.SubscriptionStatus
|
37
|
+
SubscriptionTier = Subscription.SubscriptionTier
|
38
|
+
EventType = PaymentEvent.EventType
|
39
|
+
|
31
40
|
__all__ = [
|
32
41
|
# Base
|
33
42
|
'TimestampedModel',
|
@@ -46,4 +55,13 @@ __all__ = [
|
|
46
55
|
'TariffEndpointGroup',
|
47
56
|
'APIKey',
|
48
57
|
'PaymentEvent',
|
58
|
+
|
59
|
+
# TextChoices
|
60
|
+
'CurrencyType',
|
61
|
+
'PaymentStatus',
|
62
|
+
'PaymentProvider',
|
63
|
+
'TransactionType',
|
64
|
+
'SubscriptionStatus',
|
65
|
+
'SubscriptionTier',
|
66
|
+
'EventType',
|
49
67
|
]
|
@@ -5,12 +5,12 @@ API key models for the universal payments system.
|
|
5
5
|
from django.db import models
|
6
6
|
from django.contrib.auth import get_user_model
|
7
7
|
from django.utils import timezone
|
8
|
-
from .base import
|
8
|
+
from .base import UUIDTimestampedModel
|
9
9
|
|
10
10
|
User = get_user_model()
|
11
11
|
|
12
12
|
|
13
|
-
class APIKey(
|
13
|
+
class APIKey(UUIDTimestampedModel):
|
14
14
|
"""API keys for authentication and usage tracking."""
|
15
15
|
|
16
16
|
user = models.ForeignKey(
|
@@ -7,7 +7,7 @@ from django.contrib.auth import get_user_model
|
|
7
7
|
from django.core.validators import MinValueValidator
|
8
8
|
from django.core.exceptions import ValidationError
|
9
9
|
from django.utils import timezone
|
10
|
-
from .base import TimestampedModel
|
10
|
+
from .base import UUIDTimestampedModel, TimestampedModel
|
11
11
|
|
12
12
|
User = get_user_model()
|
13
13
|
|
@@ -89,7 +89,7 @@ class UserBalance(TimestampedModel):
|
|
89
89
|
}
|
90
90
|
|
91
91
|
|
92
|
-
class Transaction(
|
92
|
+
class Transaction(UUIDTimestampedModel):
|
93
93
|
"""Transaction history model."""
|
94
94
|
|
95
95
|
class TransactionType(models.TextChoices):
|
@@ -2,6 +2,7 @@
|
|
2
2
|
Base model classes for the universal payments system.
|
3
3
|
"""
|
4
4
|
|
5
|
+
import uuid
|
5
6
|
from django.db import models
|
6
7
|
|
7
8
|
|
@@ -10,5 +11,20 @@ class TimestampedModel(models.Model):
|
|
10
11
|
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
11
12
|
updated_at = models.DateTimeField(auto_now=True)
|
12
13
|
|
14
|
+
class Meta:
|
15
|
+
abstract = True
|
16
|
+
|
17
|
+
|
18
|
+
class UUIDTimestampedModel(models.Model):
|
19
|
+
"""Base model with UUID primary key and automatic timestamps."""
|
20
|
+
id = models.UUIDField(
|
21
|
+
primary_key=True,
|
22
|
+
default=uuid.uuid4,
|
23
|
+
editable=False,
|
24
|
+
help_text="Unique identifier"
|
25
|
+
)
|
26
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
27
|
+
updated_at = models.DateTimeField(auto_now=True)
|
28
|
+
|
13
29
|
class Meta:
|
14
30
|
abstract = True
|
@@ -3,10 +3,10 @@ Event sourcing models for the universal payments system.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from django.db import models
|
6
|
-
from .base import
|
6
|
+
from .base import UUIDTimestampedModel
|
7
7
|
|
8
8
|
|
9
|
-
class PaymentEvent(
|
9
|
+
class PaymentEvent(UUIDTimestampedModel):
|
10
10
|
"""Event sourcing for payment operations - immutable audit trail."""
|
11
11
|
|
12
12
|
class EventType(models.TextChoices):
|
@@ -7,13 +7,13 @@ 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
|
10
|
+
from .base import UUIDTimestampedModel
|
11
11
|
|
12
12
|
User = get_user_model()
|
13
13
|
|
14
14
|
|
15
15
|
|
16
|
-
class UniversalPayment(
|
16
|
+
class UniversalPayment(UUIDTimestampedModel):
|
17
17
|
"""Universal payment model for all providers."""
|
18
18
|
|
19
19
|
class PaymentStatus(models.TextChoices):
|
@@ -7,7 +7,7 @@ from django.contrib.auth import get_user_model
|
|
7
7
|
from django.core.validators import MinValueValidator
|
8
8
|
from django.utils import timezone
|
9
9
|
from datetime import timedelta
|
10
|
-
from .base import TimestampedModel
|
10
|
+
from .base import UUIDTimestampedModel, TimestampedModel
|
11
11
|
|
12
12
|
User = get_user_model()
|
13
13
|
|
@@ -106,7 +106,7 @@ class EndpointGroup(TimestampedModel):
|
|
106
106
|
return tier_limits.get(tier, 0)
|
107
107
|
|
108
108
|
|
109
|
-
class Subscription(
|
109
|
+
class Subscription(UUIDTimestampedModel):
|
110
110
|
"""User subscriptions to endpoint groups."""
|
111
111
|
|
112
112
|
class SubscriptionStatus(models.TextChoices):
|
@@ -1,14 +1,65 @@
|
|
1
1
|
"""
|
2
|
-
Universal
|
2
|
+
Universal Payment Services.
|
3
|
+
|
4
|
+
Modular architecture with minimal Pydantic typing for inter-service communication.
|
5
|
+
Uses Django ORM for data persistence and DRF for API responses.
|
3
6
|
"""
|
4
7
|
|
5
|
-
|
6
|
-
from .
|
7
|
-
from .
|
8
|
+
# Core services
|
9
|
+
from .core.payment_service import PaymentService
|
10
|
+
from .core.balance_service import BalanceService
|
11
|
+
from .core.subscription_service import SubscriptionService
|
12
|
+
|
13
|
+
# Provider services
|
14
|
+
from .providers.registry import ProviderRegistry
|
15
|
+
from .providers.nowpayments import NowPaymentsProvider
|
16
|
+
from .providers.cryptapi import CryptAPIProvider
|
17
|
+
|
18
|
+
# Cache services
|
19
|
+
from .cache import SimpleCache, ApiKeyCache, RateLimitCache
|
20
|
+
|
21
|
+
# Internal types for inter-service communication
|
22
|
+
from .internal_types import (
|
23
|
+
ProviderResponse, WebhookData, ServiceOperationResult,
|
24
|
+
BalanceUpdateRequest, AccessCheckRequest, AccessCheckResult,
|
25
|
+
# Service response models
|
26
|
+
PaymentCreationResult, WebhookProcessingResult, PaymentStatusResult,
|
27
|
+
UserBalanceResult, TransferResult, TransactionInfo,
|
28
|
+
EndpointGroupInfo, SubscriptionInfo, SubscriptionAnalytics
|
29
|
+
)
|
8
30
|
|
9
31
|
__all__ = [
|
10
|
-
|
11
|
-
'PaymentService',
|
32
|
+
# Core services
|
33
|
+
'PaymentService',
|
34
|
+
'BalanceService',
|
35
|
+
'SubscriptionService',
|
36
|
+
|
37
|
+
# Provider services
|
38
|
+
'ProviderRegistry',
|
12
39
|
'NowPaymentsProvider',
|
13
|
-
'
|
40
|
+
'CryptAPIProvider',
|
41
|
+
|
42
|
+
# Cache services
|
43
|
+
'SimpleCache',
|
44
|
+
'ApiKeyCache',
|
45
|
+
'RateLimitCache',
|
46
|
+
|
47
|
+
# Internal types
|
48
|
+
'ProviderResponse',
|
49
|
+
'WebhookData',
|
50
|
+
'ServiceOperationResult',
|
51
|
+
'BalanceUpdateRequest',
|
52
|
+
'AccessCheckRequest',
|
53
|
+
'AccessCheckResult',
|
54
|
+
|
55
|
+
# Service response models
|
56
|
+
'PaymentCreationResult',
|
57
|
+
'WebhookProcessingResult',
|
58
|
+
'PaymentStatusResult',
|
59
|
+
'UserBalanceResult',
|
60
|
+
'TransferResult',
|
61
|
+
'TransactionInfo',
|
62
|
+
'EndpointGroupInfo',
|
63
|
+
'SubscriptionInfo',
|
64
|
+
'SubscriptionAnalytics',
|
14
65
|
]
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
Simple caching for API key access control and rate limiting.
|
3
|
+
|
4
|
+
ONLY for API key caching - NOT for payment data!
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .base import CacheInterface
|
8
|
+
from .simple_cache import SimpleCache, ApiKeyCache, RateLimitCache
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
'CacheInterface',
|
12
|
+
'SimpleCache',
|
13
|
+
'ApiKeyCache',
|
14
|
+
'RateLimitCache',
|
15
|
+
]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"""
|
2
|
+
Base cache interface for payments module.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from typing import Optional, Any
|
7
|
+
|
8
|
+
|
9
|
+
class CacheInterface(ABC):
|
10
|
+
"""Abstract cache interface."""
|
11
|
+
|
12
|
+
@abstractmethod
|
13
|
+
def get(self, key: str) -> Optional[Any]:
|
14
|
+
"""Get value from cache."""
|
15
|
+
pass
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
|
19
|
+
"""Set value in cache."""
|
20
|
+
pass
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def delete(self, key: str) -> bool:
|
24
|
+
"""Delete value from cache."""
|
25
|
+
pass
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
def exists(self, key: str) -> bool:
|
29
|
+
"""Check if key exists in cache."""
|
30
|
+
pass
|