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.
Files changed (92) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/newsletter/signals.py +9 -8
  3. django_cfg/apps/payments/__init__.py +8 -0
  4. django_cfg/apps/payments/admin/__init__.py +23 -0
  5. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  6. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  7. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  8. django_cfg/apps/payments/admin/filters.py +259 -0
  9. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  10. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  11. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  12. django_cfg/apps/payments/apps.py +22 -0
  13. django_cfg/apps/payments/config/__init__.py +87 -0
  14. django_cfg/apps/payments/config/module.py +162 -0
  15. django_cfg/apps/payments/config/providers.py +93 -0
  16. django_cfg/apps/payments/config/settings.py +136 -0
  17. django_cfg/apps/payments/config/utils.py +198 -0
  18. django_cfg/apps/payments/decorators.py +291 -0
  19. django_cfg/apps/payments/managers/__init__.py +22 -0
  20. django_cfg/apps/payments/managers/api_key_manager.py +35 -0
  21. django_cfg/apps/payments/managers/balance_manager.py +361 -0
  22. django_cfg/apps/payments/managers/currency_manager.py +32 -0
  23. django_cfg/apps/payments/managers/payment_manager.py +44 -0
  24. django_cfg/apps/payments/managers/subscription_manager.py +37 -0
  25. django_cfg/apps/payments/managers/tariff_manager.py +29 -0
  26. django_cfg/apps/payments/middleware/__init__.py +13 -0
  27. django_cfg/apps/payments/middleware/api_access.py +261 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +1003 -0
  31. django_cfg/apps/payments/migrations/__init__.py +1 -0
  32. django_cfg/apps/payments/models/__init__.py +67 -0
  33. django_cfg/apps/payments/models/api_keys.py +96 -0
  34. django_cfg/apps/payments/models/balance.py +209 -0
  35. django_cfg/apps/payments/models/base.py +30 -0
  36. django_cfg/apps/payments/models/currencies.py +138 -0
  37. django_cfg/apps/payments/models/events.py +73 -0
  38. django_cfg/apps/payments/models/payments.py +301 -0
  39. django_cfg/apps/payments/models/subscriptions.py +270 -0
  40. django_cfg/apps/payments/models/tariffs.py +102 -0
  41. django_cfg/apps/payments/serializers/__init__.py +56 -0
  42. django_cfg/apps/payments/serializers/api_keys.py +51 -0
  43. django_cfg/apps/payments/serializers/balance.py +59 -0
  44. django_cfg/apps/payments/serializers/currencies.py +55 -0
  45. django_cfg/apps/payments/serializers/payments.py +62 -0
  46. django_cfg/apps/payments/serializers/subscriptions.py +71 -0
  47. django_cfg/apps/payments/serializers/tariffs.py +56 -0
  48. django_cfg/apps/payments/services/__init__.py +65 -0
  49. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  50. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  51. django_cfg/apps/payments/services/cache/base.py +30 -0
  52. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  53. django_cfg/apps/payments/services/core/__init__.py +17 -0
  54. django_cfg/apps/payments/services/core/balance_service.py +449 -0
  55. django_cfg/apps/payments/services/core/payment_service.py +393 -0
  56. django_cfg/apps/payments/services/core/subscription_service.py +616 -0
  57. django_cfg/apps/payments/services/internal_types.py +266 -0
  58. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  59. django_cfg/apps/payments/services/providers/__init__.py +19 -0
  60. django_cfg/apps/payments/services/providers/base.py +137 -0
  61. django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
  62. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  63. django_cfg/apps/payments/services/providers/registry.py +99 -0
  64. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  65. django_cfg/apps/payments/signals/__init__.py +13 -0
  66. django_cfg/apps/payments/signals/api_key_signals.py +150 -0
  67. django_cfg/apps/payments/signals/payment_signals.py +127 -0
  68. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  69. django_cfg/apps/payments/urls.py +78 -0
  70. django_cfg/apps/payments/utils/__init__.py +42 -0
  71. django_cfg/apps/payments/utils/config_utils.py +243 -0
  72. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  73. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  74. django_cfg/apps/payments/views/__init__.py +62 -0
  75. django_cfg/apps/payments/views/api_key_views.py +164 -0
  76. django_cfg/apps/payments/views/balance_views.py +75 -0
  77. django_cfg/apps/payments/views/currency_views.py +111 -0
  78. django_cfg/apps/payments/views/payment_views.py +111 -0
  79. django_cfg/apps/payments/views/subscription_views.py +135 -0
  80. django_cfg/apps/payments/views/tariff_views.py +131 -0
  81. django_cfg/apps/support/signals.py +16 -4
  82. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  83. django_cfg/core/config.py +6 -0
  84. django_cfg/models/revolution.py +14 -0
  85. django_cfg/modules/base.py +9 -0
  86. django_cfg/modules/django_email.py +42 -4
  87. django_cfg/modules/django_unfold/dashboard.py +20 -0
  88. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
  89. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/RECORD +92 -14
  90. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
  91. {django_cfg-1.2.21.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
  92. {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
+ ]