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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- 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 +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -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 +125 -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 +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -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 +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -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 +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -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/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- 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/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {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
|
+
]
|