django-cfg 1.2.21__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/newsletter/signals.py +9 -8
- django_cfg/apps/payments/__init__.py +8 -0
- 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/apps.py +22 -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/managers/__init__.py +22 -0
- django_cfg/apps/payments/managers/api_key_manager.py +35 -0
- django_cfg/apps/payments/managers/balance_manager.py +361 -0
- django_cfg/apps/payments/managers/currency_manager.py +32 -0
- django_cfg/apps/payments/managers/payment_manager.py +44 -0
- django_cfg/apps/payments/managers/subscription_manager.py +37 -0
- django_cfg/apps/payments/managers/tariff_manager.py +29 -0
- django_cfg/apps/payments/middleware/__init__.py +13 -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 +1003 -0
- django_cfg/apps/payments/migrations/__init__.py +1 -0
- django_cfg/apps/payments/models/__init__.py +67 -0
- django_cfg/apps/payments/models/api_keys.py +96 -0
- django_cfg/apps/payments/models/balance.py +209 -0
- django_cfg/apps/payments/models/base.py +30 -0
- django_cfg/apps/payments/models/currencies.py +138 -0
- django_cfg/apps/payments/models/events.py +73 -0
- django_cfg/apps/payments/models/payments.py +301 -0
- django_cfg/apps/payments/models/subscriptions.py +270 -0
- django_cfg/apps/payments/models/tariffs.py +102 -0
- django_cfg/apps/payments/serializers/__init__.py +56 -0
- django_cfg/apps/payments/serializers/api_keys.py +51 -0
- django_cfg/apps/payments/serializers/balance.py +59 -0
- django_cfg/apps/payments/serializers/currencies.py +55 -0
- django_cfg/apps/payments/serializers/payments.py +62 -0
- django_cfg/apps/payments/serializers/subscriptions.py +71 -0
- django_cfg/apps/payments/serializers/tariffs.py +56 -0
- django_cfg/apps/payments/services/__init__.py +65 -0
- 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 +78 -0
- 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/payments/views/__init__.py +62 -0
- django_cfg/apps/payments/views/api_key_views.py +164 -0
- django_cfg/apps/payments/views/balance_views.py +75 -0
- django_cfg/apps/payments/views/currency_views.py +111 -0
- django_cfg/apps/payments/views/payment_views.py +111 -0
- django_cfg/apps/payments/views/subscription_views.py +135 -0
- django_cfg/apps/payments/views/tariff_views.py +131 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/core/config.py +6 -0
- django_cfg/models/revolution.py +14 -0
- django_cfg/modules/base.py +9 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,393 @@
|
|
1
|
+
"""
|
2
|
+
Payment Service - Core payment processing logic.
|
3
|
+
|
4
|
+
This service handles universal payment operations, provider orchestration,
|
5
|
+
and payment lifecycle management.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Optional, List
|
10
|
+
from decimal import Decimal
|
11
|
+
from datetime import timezone
|
12
|
+
|
13
|
+
from django.db import transaction
|
14
|
+
from django.contrib.auth import get_user_model
|
15
|
+
from pydantic import BaseModel, Field, ValidationError
|
16
|
+
|
17
|
+
from .balance_service import BalanceService
|
18
|
+
from ...models import UniversalPayment, UserBalance, Transaction
|
19
|
+
from ...utils.config_utils import get_payments_config
|
20
|
+
from ..providers.registry import ProviderRegistry
|
21
|
+
from ..internal_types import (
|
22
|
+
ProviderResponse, WebhookData, ServiceOperationResult,
|
23
|
+
BalanceUpdateRequest, AccessCheckRequest, AccessCheckResult,
|
24
|
+
PaymentCreationResult, WebhookProcessingResult, PaymentStatusResult
|
25
|
+
)
|
26
|
+
|
27
|
+
User = get_user_model()
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
class PaymentRequest(BaseModel):
|
32
|
+
"""Type-safe payment request validation"""
|
33
|
+
user_id: int = Field(gt=0, description="User ID")
|
34
|
+
amount: Decimal = Field(gt=0, description="Payment amount")
|
35
|
+
currency: str = Field(min_length=3, max_length=10, description="Currency code")
|
36
|
+
provider: str = Field(min_length=1, description="Payment provider name")
|
37
|
+
callback_url: Optional[str] = Field(None, description="Success callback URL")
|
38
|
+
cancel_url: Optional[str] = Field(None, description="Cancellation URL")
|
39
|
+
metadata: dict = Field(default_factory=dict, description="Additional metadata")
|
40
|
+
|
41
|
+
|
42
|
+
class PaymentResult(BaseModel):
|
43
|
+
"""Type-safe payment operation result"""
|
44
|
+
success: bool
|
45
|
+
payment_id: Optional[str] = None
|
46
|
+
provider_payment_id: Optional[str] = None
|
47
|
+
payment_url: Optional[str] = None
|
48
|
+
error_message: Optional[str] = None
|
49
|
+
error_code: Optional[str] = None
|
50
|
+
metadata: dict = Field(default_factory=dict)
|
51
|
+
|
52
|
+
|
53
|
+
class WebhookProcessingResult(BaseModel):
|
54
|
+
"""Type-safe webhook processing result"""
|
55
|
+
success: bool
|
56
|
+
payment_id: Optional[str] = None
|
57
|
+
status_updated: bool = False
|
58
|
+
balance_updated: bool = False
|
59
|
+
error_message: Optional[str] = None
|
60
|
+
|
61
|
+
|
62
|
+
class PaymentService:
|
63
|
+
"""
|
64
|
+
Universal payment processing service.
|
65
|
+
|
66
|
+
Handles payment creation, webhook processing, and provider management.
|
67
|
+
Integrates with balance management and caching.
|
68
|
+
"""
|
69
|
+
|
70
|
+
def __init__(self):
|
71
|
+
"""Initialize payment service with dependencies"""
|
72
|
+
self.provider_registry = ProviderRegistry()
|
73
|
+
self.config = get_payments_config()
|
74
|
+
|
75
|
+
def create_payment(self, payment_data: dict) -> 'PaymentCreationResult':
|
76
|
+
"""
|
77
|
+
Create a new payment with the specified provider.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
payment_data: Dictionary with payment details
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
PaymentCreationResult with payment details or error information
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
# Validate payment request
|
87
|
+
request = PaymentRequest(
|
88
|
+
user_id=payment_data['user_id'],
|
89
|
+
amount=payment_data['amount'],
|
90
|
+
currency=payment_data.get('currency', 'USD'),
|
91
|
+
provider=payment_data['provider'],
|
92
|
+
metadata=payment_data.get('metadata', {})
|
93
|
+
)
|
94
|
+
|
95
|
+
# Get provider instance
|
96
|
+
provider_instance = self.provider_registry.get_provider(request.provider)
|
97
|
+
if not provider_instance:
|
98
|
+
return PaymentCreationResult(
|
99
|
+
success=False,
|
100
|
+
error=f"Payment provider '{request.provider}' is not available"
|
101
|
+
)
|
102
|
+
|
103
|
+
# Get user
|
104
|
+
user = User.objects.get(id=request.user_id)
|
105
|
+
|
106
|
+
# Convert currency if needed
|
107
|
+
amount_usd = self._convert_to_usd(request.amount, request.currency) if request.currency != 'USD' else request.amount
|
108
|
+
|
109
|
+
# Create payment record
|
110
|
+
with transaction.atomic():
|
111
|
+
payment = UniversalPayment.objects.create(
|
112
|
+
user=user,
|
113
|
+
provider=request.provider,
|
114
|
+
amount_usd=amount_usd,
|
115
|
+
currency_code=request.currency,
|
116
|
+
status=UniversalPayment.PaymentStatus.PENDING,
|
117
|
+
metadata=request.metadata
|
118
|
+
)
|
119
|
+
|
120
|
+
# Prepare provider data
|
121
|
+
provider_data = {
|
122
|
+
'amount': float(request.amount),
|
123
|
+
'currency': request.currency,
|
124
|
+
'user_id': user.id,
|
125
|
+
'payment_id': str(payment.id),
|
126
|
+
'callback_url': request.callback_url,
|
127
|
+
'cancel_url': request.cancel_url,
|
128
|
+
**request.metadata
|
129
|
+
}
|
130
|
+
|
131
|
+
# Process with provider
|
132
|
+
provider_result = provider_instance.create_payment(provider_data)
|
133
|
+
|
134
|
+
if provider_result.success:
|
135
|
+
# Update payment with provider data
|
136
|
+
payment.provider_payment_id = provider_result.provider_payment_id
|
137
|
+
payment.save()
|
138
|
+
|
139
|
+
|
140
|
+
return PaymentCreationResult(
|
141
|
+
success=True,
|
142
|
+
payment_id=str(payment.id),
|
143
|
+
provider_payment_id=provider_result.provider_payment_id,
|
144
|
+
payment_url=provider_result.payment_url
|
145
|
+
)
|
146
|
+
else:
|
147
|
+
# Mark payment as failed
|
148
|
+
payment.status = UniversalPayment.PaymentStatus.FAILED
|
149
|
+
payment.error_message = provider_result.error_message or 'Unknown provider error'
|
150
|
+
payment.save()
|
151
|
+
|
152
|
+
return PaymentCreationResult(
|
153
|
+
success=False,
|
154
|
+
payment_id=str(payment.id),
|
155
|
+
error=provider_result.error_message or 'Payment creation failed'
|
156
|
+
)
|
157
|
+
|
158
|
+
except ValidationError as e:
|
159
|
+
logger.error(f"Payment validation error: {e}")
|
160
|
+
return PaymentCreationResult(
|
161
|
+
success=False,
|
162
|
+
error=f"Invalid payment data: {e}"
|
163
|
+
)
|
164
|
+
except Exception as e:
|
165
|
+
logger.error(f"Payment creation failed: {e}", exc_info=True)
|
166
|
+
return PaymentCreationResult(
|
167
|
+
success=False,
|
168
|
+
error=f"Internal error: {str(e)}"
|
169
|
+
)
|
170
|
+
|
171
|
+
def process_webhook(
|
172
|
+
self,
|
173
|
+
provider: str,
|
174
|
+
webhook_data: dict,
|
175
|
+
request_headers: Optional[dict] = None
|
176
|
+
) -> 'WebhookProcessingResult':
|
177
|
+
"""
|
178
|
+
Process payment webhook from provider.
|
179
|
+
|
180
|
+
Args:
|
181
|
+
provider: Payment provider name
|
182
|
+
webhook_data: Webhook payload data
|
183
|
+
request_headers: HTTP headers for validation
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
WebhookProcessingResult with processing status
|
187
|
+
"""
|
188
|
+
try:
|
189
|
+
# Get provider instance
|
190
|
+
provider_instance = self.provider_registry.get_provider(provider)
|
191
|
+
if not provider_instance:
|
192
|
+
return WebhookProcessingResult(
|
193
|
+
success=False,
|
194
|
+
error=f"Provider '{provider}' not found"
|
195
|
+
)
|
196
|
+
|
197
|
+
# Process webhook with provider
|
198
|
+
webhook_result = provider_instance.process_webhook(webhook_data)
|
199
|
+
if not webhook_result.success:
|
200
|
+
return WebhookProcessingResult(
|
201
|
+
success=False,
|
202
|
+
error=webhook_result.error_message or "Webhook processing failed"
|
203
|
+
)
|
204
|
+
|
205
|
+
# Find payment by provider payment ID
|
206
|
+
try:
|
207
|
+
payment = UniversalPayment.objects.get(
|
208
|
+
provider_payment_id=webhook_result.provider_payment_id
|
209
|
+
)
|
210
|
+
except UniversalPayment.DoesNotExist:
|
211
|
+
return WebhookProcessingResult(
|
212
|
+
success=False,
|
213
|
+
error=f"Payment not found: {webhook_result.provider_payment_id}"
|
214
|
+
)
|
215
|
+
|
216
|
+
# Process payment status update
|
217
|
+
old_status = payment.status
|
218
|
+
new_status = webhook_result.status
|
219
|
+
|
220
|
+
with transaction.atomic():
|
221
|
+
# Update payment
|
222
|
+
payment.status = new_status
|
223
|
+
payment.save()
|
224
|
+
|
225
|
+
# Process completion if status changed to completed
|
226
|
+
balance_updated = False
|
227
|
+
if (new_status == UniversalPayment.PaymentStatus.COMPLETED and
|
228
|
+
old_status != UniversalPayment.PaymentStatus.COMPLETED):
|
229
|
+
balance_updated = self._process_payment_completion(payment)
|
230
|
+
|
231
|
+
|
232
|
+
return WebhookProcessingResult(
|
233
|
+
success=True,
|
234
|
+
payment_id=str(payment.id),
|
235
|
+
status_updated=(old_status != new_status),
|
236
|
+
balance_updated=balance_updated
|
237
|
+
)
|
238
|
+
|
239
|
+
except Exception as e:
|
240
|
+
logger.error(f"Webhook processing failed for {provider}: {e}", exc_info=True)
|
241
|
+
return WebhookProcessingResult(
|
242
|
+
success=False,
|
243
|
+
error=f"Webhook processing error: {str(e)}"
|
244
|
+
)
|
245
|
+
|
246
|
+
def get_payment_status(self, payment_id: str) -> Optional['PaymentStatusResult']:
|
247
|
+
"""
|
248
|
+
Get payment status by ID.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
payment_id: Payment UUID
|
252
|
+
|
253
|
+
Returns:
|
254
|
+
Payment status information or None if not found
|
255
|
+
"""
|
256
|
+
try:
|
257
|
+
|
258
|
+
# Get from database
|
259
|
+
payment = UniversalPayment.objects.get(id=payment_id)
|
260
|
+
|
261
|
+
return PaymentStatusResult(
|
262
|
+
payment_id=str(payment.id),
|
263
|
+
status=payment.status,
|
264
|
+
amount_usd=payment.amount_usd,
|
265
|
+
currency_code=payment.currency_code,
|
266
|
+
provider=payment.provider,
|
267
|
+
provider_payment_id=payment.provider_payment_id,
|
268
|
+
created_at=payment.created_at,
|
269
|
+
updated_at=payment.updated_at
|
270
|
+
)
|
271
|
+
|
272
|
+
except UniversalPayment.DoesNotExist:
|
273
|
+
return None
|
274
|
+
except Exception as e:
|
275
|
+
logger.error(f"Error getting payment status {payment_id}: {e}")
|
276
|
+
return None
|
277
|
+
|
278
|
+
def get_user_payments(
|
279
|
+
self,
|
280
|
+
user: User,
|
281
|
+
status: Optional[str] = None,
|
282
|
+
limit: int = 50,
|
283
|
+
offset: int = 0
|
284
|
+
) -> List[dict]:
|
285
|
+
"""
|
286
|
+
Get user's payment history.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
user: User object
|
290
|
+
status: Filter by payment status
|
291
|
+
limit: Number of payments to return
|
292
|
+
offset: Pagination offset
|
293
|
+
|
294
|
+
Returns:
|
295
|
+
List of payment dictionaries
|
296
|
+
"""
|
297
|
+
try:
|
298
|
+
queryset = UniversalPayment.objects.filter(user=user)
|
299
|
+
|
300
|
+
if status:
|
301
|
+
queryset = queryset.filter(status=status)
|
302
|
+
|
303
|
+
payments = queryset.order_by('-created_at')[offset:offset+limit]
|
304
|
+
|
305
|
+
return [
|
306
|
+
{
|
307
|
+
'id': str(payment.id),
|
308
|
+
'status': payment.status,
|
309
|
+
'amount_usd': str(payment.amount_usd),
|
310
|
+
'pay_amount': str(payment.pay_amount) if payment.pay_amount else str(payment.amount_usd),
|
311
|
+
'currency_code': payment.currency_code,
|
312
|
+
'provider': payment.provider.name if payment.provider else None,
|
313
|
+
'created_at': payment.created_at.isoformat(),
|
314
|
+
'processed_at': payment.processed_at.isoformat() if payment.processed_at else None
|
315
|
+
}
|
316
|
+
for payment in payments
|
317
|
+
]
|
318
|
+
|
319
|
+
except Exception as e:
|
320
|
+
logger.error(f"Error getting user payments for {user.id}: {e}")
|
321
|
+
return []
|
322
|
+
|
323
|
+
def _process_payment_completion(self, payment: UniversalPayment) -> bool:
|
324
|
+
"""
|
325
|
+
Process completed payment by adding funds to user balance.
|
326
|
+
|
327
|
+
Args:
|
328
|
+
payment: Completed payment object
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
True if balance was updated, False otherwise
|
332
|
+
"""
|
333
|
+
try:
|
334
|
+
|
335
|
+
balance_service = BalanceService()
|
336
|
+
result = balance_service.add_funds(
|
337
|
+
user=payment.user,
|
338
|
+
amount=payment.amount_usd,
|
339
|
+
currency_code='USD',
|
340
|
+
source='payment',
|
341
|
+
reference_id=str(payment.id),
|
342
|
+
metadata={
|
343
|
+
'provider': payment.provider.name if payment.provider else 'unknown',
|
344
|
+
'provider_payment_id': payment.provider_payment_id,
|
345
|
+
'pay_amount': str(payment.pay_amount) if payment.pay_amount else str(payment.amount_usd),
|
346
|
+
'currency_code': payment.currency_code
|
347
|
+
}
|
348
|
+
)
|
349
|
+
|
350
|
+
|
351
|
+
return result.success
|
352
|
+
|
353
|
+
except Exception as e:
|
354
|
+
logger.error(f"Error processing payment completion {payment.id}: {e}")
|
355
|
+
return False
|
356
|
+
|
357
|
+
def _convert_to_usd(self, amount: Decimal, currency: str) -> Decimal:
|
358
|
+
"""
|
359
|
+
Convert amount to USD using current exchange rates.
|
360
|
+
|
361
|
+
Args:
|
362
|
+
amount: Amount to convert
|
363
|
+
currency: Source currency
|
364
|
+
|
365
|
+
Returns:
|
366
|
+
Amount in USD
|
367
|
+
"""
|
368
|
+
if currency == 'USD':
|
369
|
+
return amount
|
370
|
+
|
371
|
+
# TODO: Implement currency conversion using exchange rate API
|
372
|
+
# For now, return the same amount (assuming USD)
|
373
|
+
logger.warning(f"Currency conversion not implemented for {currency}, using 1:1 rate")
|
374
|
+
return amount
|
375
|
+
|
376
|
+
|
377
|
+
def list_available_providers(self) -> List[dict]:
|
378
|
+
"""
|
379
|
+
List all available payment providers.
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
List of provider information
|
383
|
+
"""
|
384
|
+
return [
|
385
|
+
{
|
386
|
+
'name': name,
|
387
|
+
'display_name': provider.get_display_name(),
|
388
|
+
'supported_currencies': provider.get_supported_currencies(),
|
389
|
+
'is_active': provider.is_active(),
|
390
|
+
'provider_type': provider.get_provider_type()
|
391
|
+
}
|
392
|
+
for name, provider in self.provider_registry.get_all_providers().items()
|
393
|
+
]
|