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,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
|
+
)
|
django_cfg/apps/payments/urls.py
CHANGED
@@ -66,13 +66,13 @@ generic_patterns = [
|
|
66
66
|
|
67
67
|
urlpatterns = [
|
68
68
|
# Include all router URLs
|
69
|
-
path('
|
69
|
+
path('', include(router.urls)),
|
70
70
|
|
71
71
|
# Include nested router URLs
|
72
|
-
path('
|
73
|
-
path('
|
74
|
-
path('
|
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('
|
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
|
+
]
|