django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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 (125) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -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 +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,576 @@
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 django.db import transaction
12
+ from django.contrib.auth import get_user_model
13
+ from django.utils import timezone
14
+ from pydantic import BaseModel, Field, ValidationError
15
+
16
+ from .balance_service import BalanceService
17
+ from .fallback_service import get_fallback_service
18
+ from ...models import UniversalPayment, UserBalance, Transaction
19
+ from ...utils.config_utils import get_payments_config
20
+ from ..providers.registry import ProviderRegistry
21
+ from ..monitoring.provider_health import get_health_monitor
22
+ from ..internal_types import (
23
+ ProviderResponse, WebhookData, ServiceOperationResult,
24
+ BalanceUpdateRequest, AccessCheckRequest, AccessCheckResult,
25
+ PaymentCreationResult, WebhookProcessingResult, PaymentStatusResult,
26
+ PaymentHistoryItem, ProviderInfo
27
+ )
28
+
29
+ # Import django_currency module for currency conversion
30
+ from django_cfg.modules.django_currency import convert_currency, CurrencyError
31
+
32
+ User = get_user_model()
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class PaymentRequest(BaseModel):
37
+ """Type-safe payment request validation"""
38
+ user_id: int = Field(gt=0, description="User ID")
39
+ amount: Decimal = Field(gt=0, description="Payment amount")
40
+ currency: str = Field(min_length=3, max_length=10, description="Currency code")
41
+ provider: str = Field(min_length=1, description="Payment provider name")
42
+ callback_url: Optional[str] = Field(None, description="Success callback URL")
43
+ cancel_url: Optional[str] = Field(None, description="Cancellation URL")
44
+ metadata: dict = Field(default_factory=dict, description="Additional metadata")
45
+
46
+
47
+ class PaymentResult(BaseModel):
48
+ """Type-safe payment operation result"""
49
+ success: bool
50
+ payment_id: Optional[str] = None
51
+ provider_payment_id: Optional[str] = None
52
+ payment_url: Optional[str] = None
53
+ error_message: Optional[str] = None
54
+ error_code: Optional[str] = None
55
+ metadata: dict = Field(default_factory=dict)
56
+
57
+
58
+ class WebhookProcessingResult(BaseModel):
59
+ """Type-safe webhook processing result"""
60
+ success: bool
61
+ payment_id: Optional[str] = None
62
+ status_updated: bool = False
63
+ balance_updated: bool = False
64
+ error_message: Optional[str] = None
65
+
66
+
67
+ class PaymentService:
68
+ """
69
+ Universal payment processing service.
70
+
71
+ Handles payment creation, webhook processing, and provider management.
72
+ Integrates with balance management and caching.
73
+ """
74
+
75
+ def __init__(self):
76
+ """Initialize payment service with dependencies"""
77
+ self.provider_registry = ProviderRegistry()
78
+ self.config = get_payments_config()
79
+
80
+ def create_payment(self, payment_data: dict) -> 'PaymentCreationResult':
81
+ """
82
+ Create a new payment with the specified provider.
83
+
84
+ Args:
85
+ payment_data: Dictionary with payment details
86
+
87
+ Returns:
88
+ PaymentCreationResult with payment details or error information
89
+ """
90
+ try:
91
+ # Validate payment request
92
+ request = PaymentRequest(
93
+ user_id=payment_data['user_id'],
94
+ amount=payment_data['amount'],
95
+ currency=payment_data.get('currency', 'USD'),
96
+ provider=payment_data['provider'],
97
+ metadata=payment_data.get('metadata', {})
98
+ )
99
+
100
+ # Get provider instance
101
+ provider_instance = self.provider_registry.get_provider(request.provider)
102
+ if not provider_instance:
103
+ return PaymentCreationResult(
104
+ success=False,
105
+ error=f"Payment provider '{request.provider}' is not available"
106
+ )
107
+
108
+ # Get user
109
+ user = User.objects.get(id=request.user_id)
110
+
111
+ # Convert currency if needed
112
+ amount_usd = self._convert_to_usd(request.amount, request.currency) if request.currency != 'USD' else request.amount
113
+
114
+ # Create payment record
115
+ with transaction.atomic():
116
+ payment = UniversalPayment.objects.create(
117
+ user=user,
118
+ provider=request.provider,
119
+ amount_usd=amount_usd,
120
+ currency_code=request.currency,
121
+ status=UniversalPayment.PaymentStatus.PENDING,
122
+ metadata=request.metadata
123
+ )
124
+
125
+ # Prepare provider data
126
+ provider_data = {
127
+ 'amount': float(request.amount),
128
+ 'currency': request.currency,
129
+ 'user_id': user.id,
130
+ 'payment_id': str(payment.id),
131
+ 'callback_url': request.callback_url,
132
+ 'cancel_url': request.cancel_url,
133
+ **request.metadata
134
+ }
135
+
136
+ # Process with provider
137
+ provider_result = provider_instance.create_payment(provider_data)
138
+
139
+ if provider_result.success:
140
+ # Update payment with provider data
141
+ payment.provider_payment_id = provider_result.provider_payment_id
142
+ payment.save()
143
+
144
+
145
+ return PaymentCreationResult(
146
+ success=True,
147
+ payment_id=str(payment.id),
148
+ provider_payment_id=provider_result.provider_payment_id,
149
+ payment_url=provider_result.payment_url
150
+ )
151
+ else:
152
+ # Mark payment as failed
153
+ payment.status = UniversalPayment.PaymentStatus.FAILED
154
+ payment.error_message = provider_result.error_message or 'Unknown provider error'
155
+ payment.save()
156
+
157
+ return PaymentCreationResult(
158
+ success=False,
159
+ payment_id=str(payment.id),
160
+ error=provider_result.error_message or 'Payment creation failed'
161
+ )
162
+
163
+ except ValidationError as e:
164
+ logger.error(f"Payment validation error: {e}")
165
+ return PaymentCreationResult(
166
+ success=False,
167
+ error=f"Invalid payment data: {e}"
168
+ )
169
+ except Exception as e:
170
+ logger.error(f"Payment creation failed: {e}", exc_info=True)
171
+ return PaymentCreationResult(
172
+ success=False,
173
+ error=f"Internal error: {str(e)}"
174
+ )
175
+
176
+ def process_webhook(
177
+ self,
178
+ provider: str,
179
+ webhook_data: dict,
180
+ request_headers: Optional[dict] = None
181
+ ) -> 'WebhookProcessingResult':
182
+ """
183
+ Process payment webhook from provider.
184
+
185
+ Args:
186
+ provider: Payment provider name
187
+ webhook_data: Webhook payload data
188
+ request_headers: HTTP headers for validation
189
+
190
+ Returns:
191
+ WebhookProcessingResult with processing status
192
+ """
193
+ try:
194
+ # Get provider instance
195
+ provider_instance = self.provider_registry.get_provider(provider)
196
+ if not provider_instance:
197
+ return WebhookProcessingResult(
198
+ success=False,
199
+ error=f"Provider '{provider}' not found"
200
+ )
201
+
202
+ # Process webhook with provider
203
+ webhook_result = provider_instance.process_webhook(webhook_data)
204
+ if not webhook_result.success:
205
+ return WebhookProcessingResult(
206
+ success=False,
207
+ error=webhook_result.error_message or "Webhook processing failed"
208
+ )
209
+
210
+ # Find payment by provider payment ID
211
+ try:
212
+ payment = UniversalPayment.objects.get(
213
+ provider_payment_id=webhook_result.provider_payment_id
214
+ )
215
+ except UniversalPayment.DoesNotExist:
216
+ return WebhookProcessingResult(
217
+ success=False,
218
+ error=f"Payment not found: {webhook_result.provider_payment_id}"
219
+ )
220
+
221
+ # Process payment status update
222
+ old_status = payment.status
223
+ new_status = webhook_result.status
224
+
225
+ with transaction.atomic():
226
+ # Update payment
227
+ payment.status = new_status
228
+ payment.save()
229
+
230
+ # Process completion if status changed to completed
231
+ balance_updated = False
232
+ if (new_status == UniversalPayment.PaymentStatus.COMPLETED and
233
+ old_status != UniversalPayment.PaymentStatus.COMPLETED):
234
+ balance_updated = self._process_payment_completion(payment)
235
+
236
+
237
+ return WebhookProcessingResult(
238
+ success=True,
239
+ payment_id=str(payment.id),
240
+ status_updated=(old_status != new_status),
241
+ balance_updated=balance_updated
242
+ )
243
+
244
+ except Exception as e:
245
+ logger.error(f"Webhook processing failed for {provider}: {e}", exc_info=True)
246
+ return WebhookProcessingResult(
247
+ success=False,
248
+ error=f"Webhook processing error: {str(e)}"
249
+ )
250
+
251
+ def get_payment_status(self, payment_id: str) -> Optional['PaymentStatusResult']:
252
+ """
253
+ Get payment status by ID.
254
+
255
+ Args:
256
+ payment_id: Payment UUID
257
+
258
+ Returns:
259
+ Payment status information or None if not found
260
+ """
261
+ try:
262
+
263
+ # Get from database
264
+ payment = UniversalPayment.objects.get(id=payment_id)
265
+
266
+ return PaymentStatusResult(
267
+ payment_id=str(payment.id),
268
+ status=payment.status,
269
+ amount_usd=payment.amount_usd,
270
+ currency_code=payment.currency_code,
271
+ provider=payment.provider,
272
+ provider_payment_id=payment.provider_payment_id,
273
+ created_at=payment.created_at,
274
+ updated_at=payment.updated_at
275
+ )
276
+
277
+ except UniversalPayment.DoesNotExist:
278
+ return None
279
+ except Exception as e:
280
+ logger.error(f"Error getting payment status {payment_id}: {e}")
281
+ return None
282
+
283
+ def get_user_payments(
284
+ self,
285
+ user: User,
286
+ status: Optional[str] = None,
287
+ limit: int = 50,
288
+ offset: int = 0
289
+ ) -> List[PaymentHistoryItem]:
290
+ """
291
+ Get user's payment history.
292
+
293
+ Args:
294
+ user: User object
295
+ status: Filter by payment status
296
+ limit: Number of payments to return
297
+ offset: Pagination offset
298
+
299
+ Returns:
300
+ List of PaymentHistoryItem objects
301
+ """
302
+ try:
303
+ queryset = UniversalPayment.objects.filter(user=user)
304
+
305
+ if status:
306
+ queryset = queryset.filter(status=status)
307
+
308
+ payments = queryset.order_by('-created_at')[offset:offset+limit]
309
+
310
+ return [
311
+ PaymentHistoryItem(
312
+ id=str(payment.id),
313
+ user_id=payment.user.id,
314
+ amount=payment.pay_amount if payment.pay_amount else payment.amount_usd,
315
+ currency=payment.currency_code,
316
+ status=payment.status,
317
+ provider=payment.provider.name if payment.provider else 'unknown',
318
+ provider_payment_id=payment.provider_payment_id,
319
+ created_at=payment.created_at,
320
+ updated_at=payment.updated_at,
321
+ metadata=payment.metadata or {}
322
+ )
323
+ for payment in payments
324
+ ]
325
+
326
+ except Exception as e:
327
+ logger.error(f"Error getting user payments for {user.id}: {e}")
328
+ return []
329
+
330
+ def _process_payment_completion(self, payment: UniversalPayment) -> bool:
331
+ """
332
+ Process completed payment by adding funds to user balance.
333
+
334
+ Args:
335
+ payment: Completed payment object
336
+
337
+ Returns:
338
+ True if balance was updated, False otherwise
339
+ """
340
+ try:
341
+
342
+ balance_service = BalanceService()
343
+ result = balance_service.add_funds(
344
+ user=payment.user,
345
+ amount=payment.amount_usd,
346
+ currency_code='USD',
347
+ source='payment',
348
+ reference_id=str(payment.id),
349
+ metadata={
350
+ 'provider': payment.provider.name if payment.provider else 'unknown',
351
+ 'provider_payment_id': payment.provider_payment_id,
352
+ 'pay_amount': str(payment.pay_amount) if payment.pay_amount else str(payment.amount_usd),
353
+ 'currency_code': payment.currency_code
354
+ }
355
+ )
356
+
357
+
358
+ return result.success
359
+
360
+ except Exception as e:
361
+ logger.error(f"Error processing payment completion {payment.id}: {e}")
362
+ return False
363
+
364
+ def _convert_to_usd(self, amount: Decimal, currency: str) -> Decimal:
365
+ """
366
+ Convert amount to USD using django_currency module.
367
+
368
+ Args:
369
+ amount: Amount to convert
370
+ currency: Source currency
371
+
372
+ Returns:
373
+ Amount in USD
374
+ """
375
+ if currency == 'USD':
376
+ return amount
377
+
378
+ try:
379
+ # Use django_currency module for conversion
380
+ converted_amount = convert_currency(
381
+ amount=float(amount),
382
+ from_currency=currency,
383
+ to_currency='USD'
384
+ )
385
+
386
+ logger.info(f"Currency conversion: {amount} {currency} = {converted_amount} USD")
387
+ return Decimal(str(converted_amount))
388
+
389
+ except CurrencyError as e:
390
+ logger.error(f"Currency conversion failed for {amount} {currency} to USD: {e}")
391
+ # Fallback to 1:1 rate if conversion fails
392
+ logger.warning(f"Using 1:1 fallback rate for {currency} to USD")
393
+ return amount
394
+
395
+ except Exception as e:
396
+ logger.error(f"Unexpected error in currency conversion: {e}")
397
+ # Fallback to 1:1 rate for any other errors
398
+ logger.warning(f"Using 1:1 fallback rate for {currency} to USD due to error")
399
+ return amount
400
+
401
+ def process_webhook(self, provider: str, webhook_data: dict, headers: dict = None) -> 'WebhookProcessingResult':
402
+ """
403
+ Process webhook from payment provider.
404
+
405
+ Args:
406
+ provider: Provider name
407
+ webhook_data: Webhook payload
408
+ headers: Request headers for validation
409
+
410
+ Returns:
411
+ WebhookProcessingResult with processing status
412
+ """
413
+ try:
414
+ # Get provider instance for validation
415
+ provider_instance = self.provider_registry.get_provider(provider)
416
+ if not provider_instance:
417
+ return WebhookProcessingResult(
418
+ success=False,
419
+ error_message=f"Unknown provider: {provider}"
420
+ )
421
+
422
+ # Validate webhook
423
+ if hasattr(provider_instance, 'validate_webhook'):
424
+ is_valid = provider_instance.validate_webhook(webhook_data, headers)
425
+ if not is_valid:
426
+ logger.warning(f"Invalid webhook from {provider}: {webhook_data}")
427
+ return WebhookProcessingResult(
428
+ success=False,
429
+ error_message="Webhook validation failed"
430
+ )
431
+
432
+ # Process webhook data
433
+ processed_data = provider_instance.process_webhook(webhook_data)
434
+
435
+ # Find payment record
436
+ payment_id = processed_data.payment_id
437
+ if not payment_id:
438
+ return WebhookProcessingResult(
439
+ success=False,
440
+ error_message="No payment ID found in webhook"
441
+ )
442
+
443
+ # Update payment
444
+ with transaction.atomic():
445
+ try:
446
+ payment = UniversalPayment.objects.get(
447
+ provider_payment_id=payment_id,
448
+ provider=provider
449
+ )
450
+
451
+ # Update payment status and data
452
+ old_status = payment.status
453
+ payment.update_from_webhook(webhook_data)
454
+
455
+ # Create event for audit trail
456
+ self._create_payment_event(
457
+ payment=payment,
458
+ event_type='webhook_processed',
459
+ data={
460
+ 'provider': provider,
461
+ 'old_status': old_status,
462
+ 'new_status': payment.status,
463
+ 'webhook_data': webhook_data
464
+ }
465
+ )
466
+
467
+ # Process completion if needed
468
+ if payment.is_completed and old_status != payment.status:
469
+ success = self._process_payment_completion(payment)
470
+ if success:
471
+ payment.processed_at = timezone.now()
472
+ payment.save()
473
+
474
+ return WebhookProcessingResult(
475
+ success=True,
476
+ payment_id=str(payment.id),
477
+ new_status=payment.status
478
+ )
479
+
480
+ except UniversalPayment.DoesNotExist:
481
+ logger.error(f"Payment not found for webhook: provider={provider}, payment_id={payment_id}")
482
+ return WebhookProcessingResult(
483
+ success=False,
484
+ error_message="Payment not found"
485
+ )
486
+
487
+ except Exception as e:
488
+ logger.error(f"Error processing webhook from {provider}: {e}")
489
+ return WebhookProcessingResult(
490
+ success=False,
491
+ error_message=str(e)
492
+ )
493
+
494
+ def _create_payment_event(self, payment: UniversalPayment, event_type: str, data: dict):
495
+ """
496
+ Create payment event for audit trail.
497
+
498
+ Args:
499
+ payment: Payment object
500
+ event_type: Type of event
501
+ data: Event data
502
+ """
503
+ try:
504
+ from ...models.events import PaymentEvent
505
+
506
+ # Get next sequence number
507
+ last_event = PaymentEvent.objects.filter(
508
+ payment_id=str(payment.id)
509
+ ).order_by('-sequence_number').first()
510
+
511
+ sequence_number = (last_event.sequence_number + 1) if last_event else 1
512
+
513
+ PaymentEvent.objects.create(
514
+ payment_id=str(payment.id),
515
+ event_type=event_type,
516
+ sequence_number=sequence_number,
517
+ event_data=data,
518
+ processed_by=f"payment_service_{timezone.now().timestamp()}",
519
+ correlation_id=data.get('correlation_id'),
520
+ idempotency_key=f"{payment.id}_{event_type}_{sequence_number}"
521
+ )
522
+
523
+ except Exception as e:
524
+ logger.error(f"Failed to create payment event: {e}")
525
+
526
+ def get_payment_events(self, payment_id: str) -> List[dict]:
527
+ """
528
+ Get all events for a payment.
529
+
530
+ Args:
531
+ payment_id: Payment ID
532
+
533
+ Returns:
534
+ List of payment events
535
+ """
536
+ try:
537
+ from ...models.events import PaymentEvent
538
+
539
+ events = PaymentEvent.objects.filter(
540
+ payment_id=payment_id
541
+ ).order_by('sequence_number')
542
+
543
+ return [
544
+ {
545
+ 'id': str(event.id),
546
+ 'event_type': event.event_type,
547
+ 'sequence_number': event.sequence_number,
548
+ 'event_data': event.event_data,
549
+ 'created_at': event.created_at,
550
+ 'processed_by': event.processed_by
551
+ }
552
+ for event in events
553
+ ]
554
+
555
+ except Exception as e:
556
+ logger.error(f"Error getting payment events for {payment_id}: {e}")
557
+ return []
558
+
559
+
560
+ def list_available_providers(self) -> List[ProviderInfo]:
561
+ """
562
+ List all available payment providers.
563
+
564
+ Returns:
565
+ List of ProviderInfo objects
566
+ """
567
+ return [
568
+ ProviderInfo(
569
+ name=name,
570
+ display_name=provider.get_display_name(),
571
+ supported_currencies=provider.get_supported_currencies(),
572
+ is_active=provider.is_active(),
573
+ features={'provider_type': provider.get_provider_type()}
574
+ )
575
+ for name, provider in self.provider_registry.get_all_providers().items()
576
+ ]