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,128 @@
1
+ """
2
+ 🔄 Universal Payment Signals
3
+
4
+ Automatic payment processing and balance management via Django signals.
5
+ """
6
+
7
+ from django.db.models.signals import post_save, pre_save
8
+ from django.dispatch import receiver
9
+ from django.db import transaction
10
+ from django.utils import timezone
11
+ import logging
12
+
13
+ from ..models import UniversalPayment, UserBalance, Transaction
14
+ from ..services.cache import SimpleCache
15
+ from django_cfg.core.redis import RedisService
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @receiver(pre_save, sender=UniversalPayment)
21
+ def store_original_payment_status(sender, instance, **kwargs):
22
+ """Store original payment status for change detection."""
23
+ if instance.pk:
24
+ try:
25
+ old_instance = UniversalPayment.objects.get(pk=instance.pk)
26
+ instance._original_status = old_instance.status
27
+ except UniversalPayment.DoesNotExist:
28
+ instance._original_status = None
29
+
30
+
31
+ @receiver(post_save, sender=UniversalPayment)
32
+ def process_payment_status_changes(sender, instance, created, **kwargs):
33
+ """Process payment status changes and update user balance."""
34
+ if created:
35
+ logger.info(f"New payment created: {instance.internal_payment_id} for user {instance.user.email}")
36
+ return
37
+
38
+ # Check if status changed to completed
39
+ if hasattr(instance, '_original_status'):
40
+ old_status = instance._original_status
41
+ new_status = instance.status
42
+
43
+ if old_status != new_status:
44
+ logger.info(
45
+ f"Payment status changed: {instance.internal_payment_id} "
46
+ f"for user {instance.user.email} - {old_status} → {new_status}"
47
+ )
48
+
49
+ # Process completed payment
50
+ if new_status == UniversalPayment.PaymentStatus.COMPLETED and old_status != new_status:
51
+ _process_completed_payment(instance)
52
+
53
+
54
+ def _process_completed_payment(payment: UniversalPayment):
55
+ """Process completed payment and add funds to user balance."""
56
+ try:
57
+ with transaction.atomic():
58
+ # Get or create user balance
59
+ balance, created = UserBalance.objects.get_or_create(
60
+ user=payment.user,
61
+ defaults={
62
+ 'amount_usd': 0,
63
+ 'reserved_usd': 0
64
+ }
65
+ )
66
+
67
+ # Add funds to balance
68
+ old_balance = balance.amount_usd
69
+ balance.amount_usd += payment.amount_usd
70
+ balance.save()
71
+
72
+ # Create transaction record
73
+ Transaction.objects.create(
74
+ user=payment.user,
75
+ transaction_type=Transaction.TransactionType.PAYMENT,
76
+ amount_usd=payment.amount_usd,
77
+ balance_before=old_balance,
78
+ balance_after=balance.amount_usd,
79
+ description=f"Payment completed: {payment.internal_payment_id}",
80
+ payment=payment,
81
+ metadata={
82
+ 'provider': payment.provider,
83
+ 'provider_payment_id': payment.provider_payment_id,
84
+ 'amount_usd': str(payment.amount_usd),
85
+ 'currency_code': payment.currency_code
86
+ }
87
+ )
88
+
89
+ # Mark payment as processed
90
+ payment.processed_at = timezone.now()
91
+ payment.save(update_fields=['processed_at'])
92
+
93
+ # Clear Redis cache for user
94
+ try:
95
+ redis_service = RedisService()
96
+ redis_service.invalidate_user_cache(payment.user.id)
97
+ except Exception as e:
98
+ logger.warning(f"Failed to clear Redis cache for user {payment.user.id}: {e}")
99
+
100
+ logger.info(
101
+ f"Payment {payment.internal_payment_id} processed successfully. "
102
+ f"User {payment.user.email} balance: ${balance.amount_usd}"
103
+ )
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error processing completed payment {payment.internal_payment_id}: {e}")
107
+ raise
108
+
109
+
110
+ @receiver(post_save, sender=UniversalPayment)
111
+ def log_payment_webhook_data(sender, instance, created, **kwargs):
112
+ """Log webhook data for audit purposes."""
113
+ if not created and instance.webhook_data:
114
+ logger.info(
115
+ f"Webhook data received for payment {instance.internal_payment_id}: "
116
+ f"status={instance.status}, provider={instance.provider}"
117
+ )
118
+
119
+
120
+ @receiver(post_save, sender=Transaction)
121
+ def log_transaction_creation(sender, instance, created, **kwargs):
122
+ """Log transaction creation for audit trail."""
123
+ if created:
124
+ logger.info(
125
+ f"New transaction: {instance.transaction_type} "
126
+ f"${instance.amount_usd} for user {instance.user.email} "
127
+ f"(balance: ${instance.balance_after})"
128
+ )
@@ -0,0 +1,196 @@
1
+ """
2
+ 🔄 Universal Subscription Signals
3
+
4
+ Automatic subscription management and lifecycle handling via Django signals.
5
+ """
6
+
7
+ from django.db.models.signals import post_save, pre_save, post_delete
8
+ from django.dispatch import receiver
9
+ from django.db import transaction
10
+ from django.utils import timezone
11
+ from datetime import timedelta
12
+ import logging
13
+
14
+ from ..models import Subscription, EndpointGroup, UserBalance, Transaction
15
+ from ..services.cache import SimpleCache
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @receiver(pre_save, sender=Subscription)
21
+ def store_original_subscription_status(sender, instance, **kwargs):
22
+ """Store original subscription status for change detection."""
23
+ if instance.pk:
24
+ try:
25
+ old_instance = Subscription.objects.get(pk=instance.pk)
26
+ instance._original_status = old_instance.status
27
+ instance._original_expires_at = old_instance.expires_at
28
+ except Subscription.DoesNotExist:
29
+ instance._original_status = None
30
+ instance._original_expires_at = None
31
+
32
+
33
+ @receiver(post_save, sender=Subscription)
34
+ def process_subscription_status_changes(sender, instance, created, **kwargs):
35
+ """Process subscription status changes and handle lifecycle events."""
36
+ if created:
37
+ logger.info(
38
+ f"New subscription created: {instance.endpoint_group.name} "
39
+ f"for user {instance.user.email} (expires: {instance.expires_at})"
40
+ )
41
+ _clear_user_cache(instance.user.id)
42
+ return
43
+
44
+ # Check if status changed
45
+ if hasattr(instance, '_original_status'):
46
+ old_status = instance._original_status
47
+ new_status = instance.status
48
+
49
+ if old_status != new_status:
50
+ logger.info(
51
+ f"Subscription status changed: {instance.endpoint_group.name} "
52
+ f"for user {instance.user.email} - {old_status} → {new_status}"
53
+ )
54
+
55
+ # Handle specific status changes
56
+ if new_status == Subscription.SubscriptionStatus.ACTIVE:
57
+ _handle_subscription_activation(instance)
58
+ elif new_status == Subscription.SubscriptionStatus.CANCELLED:
59
+ _handle_subscription_cancellation(instance)
60
+ elif new_status == Subscription.SubscriptionStatus.EXPIRED:
61
+ _handle_subscription_expiration(instance)
62
+
63
+ _clear_user_cache(instance.user.id)
64
+
65
+
66
+ @receiver(post_save, sender=Subscription)
67
+ def handle_subscription_renewal(sender, instance, created, **kwargs):
68
+ """Handle subscription renewal and billing."""
69
+ if created or not hasattr(instance, '_original_expires_at'):
70
+ return
71
+
72
+ old_expires_at = instance._original_expires_at
73
+ new_expires_at = instance.expires_at
74
+
75
+ # Check if subscription was renewed (expires_at extended)
76
+ if old_expires_at and new_expires_at and new_expires_at > old_expires_at:
77
+ logger.info(
78
+ f"Subscription renewed: {instance.endpoint_group.name} "
79
+ f"for user {instance.user.email} - extended to {new_expires_at}"
80
+ )
81
+ _clear_user_cache(instance.user.id)
82
+
83
+
84
+ @receiver(post_delete, sender=Subscription)
85
+ def log_subscription_deletion(sender, instance, **kwargs):
86
+ """Log subscription deletions for audit purposes."""
87
+ logger.warning(
88
+ f"Subscription deleted: {instance.endpoint_group.name} "
89
+ f"for user {instance.user.email} - Status was: {instance.status}"
90
+ )
91
+ _clear_user_cache(instance.user.id)
92
+
93
+
94
+ @receiver(post_save, sender=EndpointGroup)
95
+ def log_endpoint_group_changes(sender, instance, created, **kwargs):
96
+ """Log endpoint group changes that may affect subscriptions."""
97
+ if created:
98
+ logger.info(f"New endpoint group created: {instance.name}")
99
+ else:
100
+ # Check if important fields changed
101
+ if instance.tracker.has_changed('is_active'):
102
+ logger.warning(
103
+ f"Endpoint group activity changed: {instance.name} "
104
+ f"- active: {instance.is_active}"
105
+ )
106
+ # Clear cache for all users with subscriptions to this group
107
+ _clear_endpoint_group_cache(instance)
108
+
109
+
110
+ def _handle_subscription_activation(subscription: Subscription):
111
+ """Handle subscription activation logic."""
112
+ try:
113
+ # Reset usage counters
114
+ subscription.usage_current = 0
115
+
116
+ # Set next billing date
117
+ if not subscription.next_billing:
118
+ subscription.next_billing = timezone.now() + timedelta(days=30) # Monthly by default
119
+
120
+ subscription.save(update_fields=['usage_current', 'next_billing'])
121
+
122
+ logger.info(f"Subscription activated: {subscription.endpoint_group.name} for {subscription.user.email}")
123
+
124
+ except Exception as e:
125
+ logger.error(f"Error handling subscription activation: {e}")
126
+
127
+
128
+ def _handle_subscription_cancellation(subscription: Subscription):
129
+ """Handle subscription cancellation logic."""
130
+ try:
131
+ # Mark as cancelled
132
+ subscription.cancelled_at = timezone.now()
133
+ subscription.save(update_fields=['cancelled_at'])
134
+
135
+ logger.info(f"Subscription cancelled: {subscription.endpoint_group.name} for {subscription.user.email}")
136
+
137
+ except Exception as e:
138
+ logger.error(f"Error handling subscription cancellation: {e}")
139
+
140
+
141
+ def _handle_subscription_expiration(subscription: Subscription):
142
+ """Handle subscription expiration logic."""
143
+ try:
144
+ # Mark as expired
145
+ subscription.expired_at = timezone.now()
146
+ subscription.save(update_fields=['expired_at'])
147
+
148
+ logger.info(f"Subscription expired: {subscription.endpoint_group.name} for {subscription.user.email}")
149
+
150
+ except Exception as e:
151
+ logger.error(f"Error handling subscription expiration: {e}")
152
+
153
+
154
+ def _clear_user_cache(user_id: int):
155
+ """Clear cache for specific user."""
156
+ try:
157
+ cache = SimpleCache("subscriptions")
158
+ cache_keys = [
159
+ f"access:{user_id}",
160
+ f"subscriptions:{user_id}",
161
+ f"user_summary:{user_id}",
162
+ ]
163
+
164
+ for key in cache_keys:
165
+ cache.delete(key)
166
+
167
+ except Exception as e:
168
+ logger.warning(f"Failed to clear cache for user {user_id}: {e}")
169
+
170
+
171
+ def _clear_endpoint_group_cache(endpoint_group: EndpointGroup):
172
+ """Clear cache for all users with subscriptions to this endpoint group."""
173
+ try:
174
+ # Get all users with active subscriptions to this group
175
+ user_ids = Subscription.objects.filter(
176
+ endpoint_group=endpoint_group,
177
+ status=Subscription.SubscriptionStatus.ACTIVE
178
+ ).values_list('user_id', flat=True)
179
+
180
+ for user_id in user_ids:
181
+ _clear_user_cache(user_id)
182
+
183
+ except Exception as e:
184
+ logger.warning(f"Failed to clear cache for endpoint group {endpoint_group.name}: {e}")
185
+
186
+
187
+ @receiver(post_save, sender=Subscription)
188
+ def update_usage_statistics(sender, instance, created, **kwargs):
189
+ """Update usage statistics when subscription is modified."""
190
+ if not created and hasattr(instance, '_original_status'):
191
+ # Only update stats if usage-related fields might have changed
192
+ if instance.usage_current != getattr(instance, '_original_usage_current', instance.usage_current):
193
+ logger.debug(
194
+ f"Usage updated for subscription {instance.endpoint_group.name}: "
195
+ f"{instance.usage_current} requests"
196
+ )
@@ -0,0 +1,12 @@
1
+ """
2
+ Payment Background Tasks
3
+
4
+ Minimal task infrastructure for webhook processing using existing Dramatiq setup.
5
+ Uses django-cfg task configuration from knowbase module.
6
+ """
7
+
8
+ from .webhook_processing import process_webhook_async
9
+
10
+ __all__ = [
11
+ 'process_webhook_async',
12
+ ]
@@ -0,0 +1,177 @@
1
+ """
2
+ Webhook Processing Tasks
3
+
4
+ Simple webhook processing with fallback to sync processing.
5
+ Uses existing Dramatiq configuration and graceful degradation.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, Any, Optional
10
+ from django.db import transaction
11
+ from django.utils import timezone
12
+
13
+ # Use existing dramatiq setup
14
+ import dramatiq
15
+
16
+ from ..services.core.payment_service import PaymentService
17
+ from ..models.events import PaymentEvent
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dramatiq.actor(
23
+ queue_name="payments",
24
+ priority=3 # High priority for webhooks
25
+ )
26
+ def process_webhook_async(
27
+ provider: str,
28
+ webhook_data: dict,
29
+ idempotency_key: str,
30
+ request_headers: Optional[dict] = None
31
+ ) -> Dict[str, Any]:
32
+ """
33
+ Process payment webhook asynchronously.
34
+
35
+ Args:
36
+ provider: Payment provider name (nowpayments, cryptapi, etc.)
37
+ webhook_data: Raw webhook payload from provider
38
+ idempotency_key: Unique key to prevent duplicate processing
39
+ request_headers: HTTP headers for webhook validation
40
+
41
+ Returns:
42
+ Processing results with success/error status
43
+
44
+ Raises:
45
+ Exception: If processing fails after retries
46
+ """
47
+ start_time = timezone.now()
48
+
49
+ try:
50
+ # Log task start
51
+ logger.info(f"🚀 Processing webhook async: {provider}, key: {idempotency_key}")
52
+
53
+ # Check for duplicate processing
54
+ if _is_webhook_already_processed(idempotency_key):
55
+ logger.info(f"✅ Webhook already processed: {idempotency_key}")
56
+ return {"success": True, "message": "Already processed", "duplicate": True}
57
+
58
+ # Process webhook
59
+ with transaction.atomic():
60
+ payment_service = PaymentService()
61
+ result = payment_service.process_webhook(
62
+ provider=provider,
63
+ webhook_data=webhook_data,
64
+ request_headers=request_headers
65
+ )
66
+
67
+ # Mark as processed
68
+ _mark_webhook_processed(idempotency_key, result.dict())
69
+
70
+ processing_time = (timezone.now() - start_time).total_seconds()
71
+
72
+ logger.info(
73
+ f"✅ Webhook processed successfully: {idempotency_key}, "
74
+ f"time: {processing_time:.2f}s"
75
+ )
76
+
77
+ return {
78
+ "success": True,
79
+ "idempotency_key": idempotency_key,
80
+ "processing_time_seconds": processing_time,
81
+ "result": result.dict()
82
+ }
83
+
84
+ except Exception as e:
85
+ processing_time = (timezone.now() - start_time).total_seconds()
86
+
87
+ logger.error(
88
+ f"❌ Webhook processing failed: {idempotency_key}, "
89
+ f"error: {str(e)}, time: {processing_time:.2f}s"
90
+ )
91
+
92
+ # Re-raise for Dramatiq retry mechanism
93
+ raise
94
+
95
+
96
+ def process_webhook_with_fallback(
97
+ provider: str,
98
+ webhook_data: dict,
99
+ idempotency_key: str,
100
+ request_headers: Optional[dict] = None,
101
+ force_sync: bool = False
102
+ ):
103
+ """
104
+ Process webhook with automatic async/sync fallback.
105
+
106
+ If Dramatiq is unavailable, processes synchronously.
107
+ If force_sync=True, skips async processing.
108
+ """
109
+ if force_sync:
110
+ logger.info(f"Processing webhook synchronously (forced): {provider}")
111
+ return _process_webhook_sync(provider, webhook_data, idempotency_key, request_headers)
112
+
113
+ try:
114
+ # Try async processing
115
+ process_webhook_async.send(
116
+ provider=provider,
117
+ webhook_data=webhook_data,
118
+ idempotency_key=idempotency_key,
119
+ request_headers=request_headers
120
+ )
121
+ logger.info(f"Webhook queued for async processing: {idempotency_key}")
122
+ return {"success": True, "mode": "async", "queued": True}
123
+
124
+ except Exception as e:
125
+ logger.warning(f"Async processing failed, falling back to sync: {e}")
126
+ return _process_webhook_sync(provider, webhook_data, idempotency_key, request_headers)
127
+
128
+
129
+ def _process_webhook_sync(
130
+ provider: str,
131
+ webhook_data: dict,
132
+ idempotency_key: str,
133
+ request_headers: Optional[dict] = None
134
+ ):
135
+ """Fallback sync webhook processing."""
136
+ logger.info(f"Processing webhook synchronously: {provider}")
137
+
138
+ try:
139
+ payment_service = PaymentService()
140
+ result = payment_service.process_webhook(
141
+ provider=provider,
142
+ webhook_data=webhook_data,
143
+ request_headers=request_headers
144
+ )
145
+
146
+ _mark_webhook_processed(idempotency_key, result.dict())
147
+
148
+ return {
149
+ "success": True,
150
+ "mode": "sync",
151
+ "result": result.dict()
152
+ }
153
+
154
+ except Exception as e:
155
+ logger.error(f"Sync webhook processing failed: {e}")
156
+ raise
157
+
158
+
159
+ def _is_webhook_already_processed(idempotency_key: str) -> bool:
160
+ """Check if webhook was already processed."""
161
+ return PaymentEvent.objects.filter(
162
+ idempotency_key=idempotency_key,
163
+ event_type=PaymentEvent.EventType.WEBHOOK_PROCESSED
164
+ ).exists()
165
+
166
+
167
+ def _mark_webhook_processed(idempotency_key: str, result_data: dict):
168
+ """Mark webhook as processed."""
169
+ import os
170
+
171
+ PaymentEvent.objects.create(
172
+ payment_id=result_data.get('payment_id', 'unknown'),
173
+ event_type=PaymentEvent.EventType.WEBHOOK_PROCESSED,
174
+ event_data=result_data,
175
+ idempotency_key=idempotency_key,
176
+ processed_by=f"worker-{os.getpid()}"
177
+ )
@@ -66,13 +66,13 @@ generic_patterns = [
66
66
 
67
67
  urlpatterns = [
68
68
  # Include all router URLs
69
- path('api/v1/', include(router.urls)),
69
+ path('', include(router.urls)),
70
70
 
71
71
  # Include nested router URLs
72
- path('api/v1/', include(payments_router.urls)),
73
- path('api/v1/', include(subscriptions_router.urls)),
74
- path('api/v1/', include(apikeys_router.urls)),
72
+ path('', include(payments_router.urls)),
73
+ path('', include(subscriptions_router.urls)),
74
+ path('', include(apikeys_router.urls)),
75
75
 
76
76
  # Include generic API endpoints
77
- path('api/v1/', include(generic_patterns)),
77
+ path('', include(generic_patterns)),
78
78
  ]
@@ -0,0 +1,45 @@
1
+ """
2
+ Utilities for universal payments.
3
+ """
4
+
5
+ from .middleware_utils import get_client_ip, is_api_request, extract_api_key
6
+ from .billing_utils import calculate_usage_cost, create_billing_transaction, calculate_subscription_refund, process_subscription_billing, get_billing_summary
7
+ from .validation_utils import validate_api_key, check_subscription_access
8
+
9
+ # Configuration utilities
10
+ from .config_utils import (
11
+ PaymentsConfigUtil,
12
+ RedisConfigHelper,
13
+ CacheConfigHelper,
14
+ ProviderConfigHelper,
15
+ get_payments_config,
16
+ is_payments_enabled,
17
+ is_debug_mode
18
+ )
19
+
20
+ __all__ = [
21
+ # Middleware utilities
22
+ 'get_client_ip',
23
+ 'is_api_request',
24
+ 'extract_api_key',
25
+
26
+ # Billing utilities
27
+ 'calculate_usage_cost',
28
+ 'create_billing_transaction',
29
+ 'calculate_subscription_refund',
30
+ 'process_subscription_billing',
31
+ 'get_billing_summary',
32
+
33
+ # Validation utilities
34
+ 'validate_api_key',
35
+ 'check_subscription_access',
36
+
37
+ # Configuration utilities
38
+ 'PaymentsConfigUtil',
39
+ 'RedisConfigHelper',
40
+ 'CacheConfigHelper',
41
+ 'ProviderConfigHelper',
42
+ 'get_payments_config',
43
+ 'is_payments_enabled',
44
+ 'is_debug_mode',
45
+ ]