django-cfg 1.2.31__py3-none-any.whl โ 1.3.1__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/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -10
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -285
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py โ cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +346 -467
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +40 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info โ django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info โ django_cfg-1.3.1.dist-info}/RECORD +153 -185
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -146
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
- django_cfg/apps/payments/managers/__init__.py +0 -23
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -63
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -63
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -122
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -66
- django_cfg/core/integration.py +0 -160
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.31.dist-info โ django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info โ django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.31.dist-info โ django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,244 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Payment statistics and analytics views.
|
3
|
-
|
4
|
-
Provides comprehensive analytics for payment performance and trends.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from django.views.generic import TemplateView
|
8
|
-
from django.utils import timezone
|
9
|
-
from django.db.models import Q
|
10
|
-
from datetime import timedelta
|
11
|
-
from .base import (
|
12
|
-
SuperuserRequiredMixin,
|
13
|
-
PaymentStatsMixin,
|
14
|
-
PaymentContextMixin,
|
15
|
-
log_view_access
|
16
|
-
)
|
17
|
-
|
18
|
-
|
19
|
-
class PaymentStatsView(
|
20
|
-
SuperuserRequiredMixin,
|
21
|
-
PaymentStatsMixin,
|
22
|
-
PaymentContextMixin,
|
23
|
-
TemplateView
|
24
|
-
):
|
25
|
-
"""Analytics and statistics view."""
|
26
|
-
|
27
|
-
template_name = 'payments/stats.html'
|
28
|
-
page_title = 'Payment Analytics'
|
29
|
-
|
30
|
-
def get_breadcrumbs(self):
|
31
|
-
return [
|
32
|
-
{'name': 'Dashboard', 'url': '/payments/admin/'},
|
33
|
-
{'name': 'Analytics', 'url': ''},
|
34
|
-
]
|
35
|
-
|
36
|
-
def get_context_data(self, **kwargs):
|
37
|
-
context = super().get_context_data(**kwargs)
|
38
|
-
|
39
|
-
# Log access for audit
|
40
|
-
log_view_access('payment_stats', self.request.user)
|
41
|
-
|
42
|
-
# Get time period from request (default to 30 days)
|
43
|
-
days = int(self.request.GET.get('days', 30))
|
44
|
-
|
45
|
-
# Calculate date ranges
|
46
|
-
now = timezone.now()
|
47
|
-
ranges = self._get_date_ranges(now, days)
|
48
|
-
|
49
|
-
# Get comprehensive statistics
|
50
|
-
stats = {
|
51
|
-
'overview': self._get_overview_stats(),
|
52
|
-
'time_periods': self._get_time_period_stats(ranges),
|
53
|
-
'providers': self._get_detailed_provider_stats(),
|
54
|
-
'status_distribution': self._get_status_distribution(),
|
55
|
-
'trends': self._get_trend_data(days),
|
56
|
-
'performance': self._get_performance_metrics(),
|
57
|
-
}
|
58
|
-
|
59
|
-
# Get common context
|
60
|
-
common_context = self.get_common_context()
|
61
|
-
|
62
|
-
context.update({
|
63
|
-
'stats': stats,
|
64
|
-
'selected_days': days,
|
65
|
-
'available_periods': [7, 30, 90, 365],
|
66
|
-
'date_ranges': ranges,
|
67
|
-
**common_context
|
68
|
-
})
|
69
|
-
|
70
|
-
return context
|
71
|
-
|
72
|
-
def _get_date_ranges(self, now, days):
|
73
|
-
"""Calculate various date ranges for statistics."""
|
74
|
-
return {
|
75
|
-
'current_period_start': now - timedelta(days=days),
|
76
|
-
'current_period_end': now,
|
77
|
-
'previous_period_start': now - timedelta(days=days * 2),
|
78
|
-
'previous_period_end': now - timedelta(days=days),
|
79
|
-
'last_7_days': now - timedelta(days=7),
|
80
|
-
'last_30_days': now - timedelta(days=30),
|
81
|
-
'last_year': now - timedelta(days=365),
|
82
|
-
}
|
83
|
-
|
84
|
-
def _get_overview_stats(self):
|
85
|
-
"""Get overall payment statistics."""
|
86
|
-
return self.get_payment_stats()
|
87
|
-
|
88
|
-
def _get_time_period_stats(self, ranges):
|
89
|
-
"""Get statistics for different time periods."""
|
90
|
-
from ...models import UniversalPayment
|
91
|
-
|
92
|
-
periods = {}
|
93
|
-
|
94
|
-
# Current period
|
95
|
-
current_qs = UniversalPayment.objects.filter(
|
96
|
-
created_at__gte=ranges['current_period_start'],
|
97
|
-
created_at__lte=ranges['current_period_end']
|
98
|
-
)
|
99
|
-
periods['current'] = self.get_payment_stats(current_qs)
|
100
|
-
|
101
|
-
# Previous period for comparison
|
102
|
-
previous_qs = UniversalPayment.objects.filter(
|
103
|
-
created_at__gte=ranges['previous_period_start'],
|
104
|
-
created_at__lte=ranges['previous_period_end']
|
105
|
-
)
|
106
|
-
periods['previous'] = self.get_payment_stats(previous_qs)
|
107
|
-
|
108
|
-
# Calculate growth rates
|
109
|
-
periods['growth'] = self._calculate_growth_rates(
|
110
|
-
periods['current'],
|
111
|
-
periods['previous']
|
112
|
-
)
|
113
|
-
|
114
|
-
# Last 7 days
|
115
|
-
last_7_qs = UniversalPayment.objects.filter(created_at__gte=ranges['last_7_days'])
|
116
|
-
periods['last_7_days'] = self.get_payment_stats(last_7_qs)
|
117
|
-
|
118
|
-
# Last 30 days
|
119
|
-
last_30_qs = UniversalPayment.objects.filter(created_at__gte=ranges['last_30_days'])
|
120
|
-
periods['last_30_days'] = self.get_payment_stats(last_30_qs)
|
121
|
-
|
122
|
-
return periods
|
123
|
-
|
124
|
-
def _get_detailed_provider_stats(self):
|
125
|
-
"""Get detailed provider statistics."""
|
126
|
-
provider_stats = self.get_provider_stats()
|
127
|
-
|
128
|
-
# Add additional metrics for each provider
|
129
|
-
for stat in provider_stats:
|
130
|
-
stat['avg_amount'] = stat['volume'] / stat['count'] if stat['count'] > 0 else 0
|
131
|
-
stat['failure_rate'] = 100 - stat['success_rate']
|
132
|
-
|
133
|
-
return provider_stats
|
134
|
-
|
135
|
-
def _get_status_distribution(self):
|
136
|
-
"""Get payment status distribution."""
|
137
|
-
from ...models import UniversalPayment
|
138
|
-
from django.db.models import Count
|
139
|
-
|
140
|
-
distribution = UniversalPayment.objects.values('status').annotate(
|
141
|
-
count=Count('id')
|
142
|
-
).order_by('-count')
|
143
|
-
|
144
|
-
total = sum(item['count'] for item in distribution)
|
145
|
-
|
146
|
-
# Add percentage
|
147
|
-
for item in distribution:
|
148
|
-
item['percentage'] = (item['count'] / total * 100) if total > 0 else 0
|
149
|
-
|
150
|
-
return distribution
|
151
|
-
|
152
|
-
def _get_trend_data(self, days):
|
153
|
-
"""Get trend data for charts."""
|
154
|
-
from ...models import UniversalPayment
|
155
|
-
from django.db.models import Count, Sum
|
156
|
-
from django.db.models.functions import TruncDate
|
157
|
-
|
158
|
-
end_date = timezone.now()
|
159
|
-
start_date = end_date - timedelta(days=days)
|
160
|
-
|
161
|
-
# Daily trends
|
162
|
-
daily_trends = UniversalPayment.objects.filter(
|
163
|
-
created_at__gte=start_date,
|
164
|
-
created_at__lte=end_date
|
165
|
-
).annotate(
|
166
|
-
date=TruncDate('created_at')
|
167
|
-
).values('date').annotate(
|
168
|
-
count=Count('id'),
|
169
|
-
volume=Sum('amount_usd'),
|
170
|
-
completed=Count('id', filter=Q(status='completed'))
|
171
|
-
).order_by('date')
|
172
|
-
|
173
|
-
# Convert to list and add calculated fields
|
174
|
-
trends = []
|
175
|
-
for item in daily_trends:
|
176
|
-
trends.append({
|
177
|
-
'date': item['date'].isoformat(),
|
178
|
-
'count': item['count'],
|
179
|
-
'volume': float(item['volume'] or 0),
|
180
|
-
'completed': item['completed'],
|
181
|
-
'success_rate': (item['completed'] / item['count'] * 100) if item['count'] > 0 else 0
|
182
|
-
})
|
183
|
-
|
184
|
-
return trends
|
185
|
-
|
186
|
-
def _get_performance_metrics(self):
|
187
|
-
"""Get performance metrics."""
|
188
|
-
from ...models import UniversalPayment, PaymentEvent
|
189
|
-
from django.db.models import Avg, Min, Max
|
190
|
-
|
191
|
-
# Average processing time (from created to completed)
|
192
|
-
completed_payments = UniversalPayment.objects.filter(
|
193
|
-
status='completed',
|
194
|
-
completed_at__isnull=False
|
195
|
-
)
|
196
|
-
|
197
|
-
processing_times = []
|
198
|
-
for payment in completed_payments[:100]: # Sample for performance
|
199
|
-
if payment.completed_at and payment.created_at:
|
200
|
-
duration = payment.completed_at - payment.created_at
|
201
|
-
processing_times.append(duration.total_seconds())
|
202
|
-
|
203
|
-
metrics = {
|
204
|
-
'avg_processing_time': sum(processing_times) / len(processing_times) if processing_times else 0,
|
205
|
-
'min_processing_time': min(processing_times) if processing_times else 0,
|
206
|
-
'max_processing_time': max(processing_times) if processing_times else 0,
|
207
|
-
}
|
208
|
-
|
209
|
-
# Convert seconds to human readable format
|
210
|
-
formatted_metrics = {}
|
211
|
-
for key, value in metrics.items():
|
212
|
-
if value > 0:
|
213
|
-
formatted_metrics[f"{key}_formatted"] = self._format_duration(value)
|
214
|
-
else:
|
215
|
-
formatted_metrics[f"{key}_formatted"] = "N/A"
|
216
|
-
|
217
|
-
# Add formatted metrics to the original metrics
|
218
|
-
metrics.update(formatted_metrics)
|
219
|
-
|
220
|
-
return metrics
|
221
|
-
|
222
|
-
def _calculate_growth_rates(self, current, previous):
|
223
|
-
"""Calculate growth rates between two periods."""
|
224
|
-
growth = {}
|
225
|
-
|
226
|
-
for key in ['total_count', 'total_volume', 'completed_count']:
|
227
|
-
current_val = current.get(key, 0)
|
228
|
-
previous_val = previous.get(key, 0)
|
229
|
-
|
230
|
-
if previous_val > 0:
|
231
|
-
growth[f"{key}_rate"] = ((current_val - previous_val) / previous_val) * 100
|
232
|
-
else:
|
233
|
-
growth[f"{key}_rate"] = 100 if current_val > 0 else 0
|
234
|
-
|
235
|
-
return growth
|
236
|
-
|
237
|
-
def _format_duration(self, seconds):
|
238
|
-
"""Format duration in seconds to human readable format."""
|
239
|
-
if seconds < 60:
|
240
|
-
return f"{seconds:.1f}s"
|
241
|
-
elif seconds < 3600:
|
242
|
-
return f"{seconds/60:.1f}m"
|
243
|
-
else:
|
244
|
-
return f"{seconds/3600:.1f}h"
|
@@ -1,181 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Utility views for payment dashboard.
|
3
|
-
|
4
|
-
Provides testing, debugging, and development functionality.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from django.views.generic import TemplateView
|
8
|
-
from django.utils import timezone
|
9
|
-
from datetime import timedelta
|
10
|
-
from .base import (
|
11
|
-
SuperuserRequiredMixin,
|
12
|
-
PaymentContextMixin,
|
13
|
-
log_view_access
|
14
|
-
)
|
15
|
-
|
16
|
-
|
17
|
-
class PaymentTestView(
|
18
|
-
SuperuserRequiredMixin,
|
19
|
-
PaymentContextMixin,
|
20
|
-
TemplateView
|
21
|
-
):
|
22
|
-
"""Test view for development and debugging purposes."""
|
23
|
-
|
24
|
-
template_name = 'payments/test.html'
|
25
|
-
page_title = 'Payment System Test'
|
26
|
-
|
27
|
-
def get_breadcrumbs(self):
|
28
|
-
return [
|
29
|
-
{'name': 'Dashboard', 'url': '/payments/admin/'},
|
30
|
-
{'name': 'System Test', 'url': ''},
|
31
|
-
]
|
32
|
-
|
33
|
-
def get_context_data(self, **kwargs):
|
34
|
-
context = super().get_context_data(**kwargs)
|
35
|
-
|
36
|
-
# Log access for audit
|
37
|
-
log_view_access('payment_test', self.request.user)
|
38
|
-
|
39
|
-
# Create sample data for testing templates
|
40
|
-
sample_data = self._generate_sample_data()
|
41
|
-
|
42
|
-
# Get system information
|
43
|
-
system_info = self._get_system_info()
|
44
|
-
|
45
|
-
# Get test scenarios
|
46
|
-
test_scenarios = self._get_test_scenarios()
|
47
|
-
|
48
|
-
# Get common context
|
49
|
-
common_context = self.get_common_context()
|
50
|
-
|
51
|
-
context.update({
|
52
|
-
'sample_data': sample_data,
|
53
|
-
'system_info': system_info,
|
54
|
-
'test_scenarios': test_scenarios,
|
55
|
-
'test_mode': True,
|
56
|
-
**common_context
|
57
|
-
})
|
58
|
-
|
59
|
-
return context
|
60
|
-
|
61
|
-
def _generate_sample_data(self):
|
62
|
-
"""Generate sample payment data for testing."""
|
63
|
-
sample_payments = []
|
64
|
-
statuses = ['pending', 'confirming', 'completed', 'failed']
|
65
|
-
providers = ['nowpayments', 'cryptapi', 'cryptomus', 'stripe']
|
66
|
-
|
67
|
-
for i in range(12):
|
68
|
-
sample_payments.append({
|
69
|
-
'id': f'sample-{i}',
|
70
|
-
'internal_payment_id': f'PAY-{1000 + i}',
|
71
|
-
'provider_payment_id': f'PROV-{2000 + i}',
|
72
|
-
'amount_usd': 50.0 + (i * 25),
|
73
|
-
'currency_code': 'USD',
|
74
|
-
'status': statuses[i % len(statuses)],
|
75
|
-
'provider': providers[i % len(providers)],
|
76
|
-
'user_email': f'user{i}@example.com',
|
77
|
-
'created_at': timezone.now() - timedelta(hours=i),
|
78
|
-
'updated_at': timezone.now() - timedelta(minutes=i * 10),
|
79
|
-
'pay_address': f'1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa{i}' if i % 2 == 0 else None,
|
80
|
-
'pay_amount': 0.001 + (i * 0.0001) if i % 2 == 0 else None,
|
81
|
-
})
|
82
|
-
|
83
|
-
return {
|
84
|
-
'payments': sample_payments,
|
85
|
-
'stats': {
|
86
|
-
'total_count': len(sample_payments),
|
87
|
-
'pending_count': len([p for p in sample_payments if p['status'] == 'pending']),
|
88
|
-
'completed_count': len([p for p in sample_payments if p['status'] == 'completed']),
|
89
|
-
'failed_count': len([p for p in sample_payments if p['status'] == 'failed']),
|
90
|
-
'total_volume': sum(p['amount_usd'] for p in sample_payments),
|
91
|
-
}
|
92
|
-
}
|
93
|
-
|
94
|
-
def _get_system_info(self):
|
95
|
-
"""Get system information for debugging."""
|
96
|
-
import django
|
97
|
-
import sys
|
98
|
-
from django.conf import settings
|
99
|
-
|
100
|
-
info = {
|
101
|
-
'django_version': django.get_version(),
|
102
|
-
'python_version': sys.version,
|
103
|
-
'debug_mode': settings.DEBUG,
|
104
|
-
'database_engine': settings.DATABASES['default']['ENGINE'],
|
105
|
-
'installed_apps': len(settings.INSTALLED_APPS),
|
106
|
-
'timezone': str(settings.TIME_ZONE),
|
107
|
-
'language': settings.LANGUAGE_CODE,
|
108
|
-
}
|
109
|
-
|
110
|
-
# Add payment-specific info
|
111
|
-
try:
|
112
|
-
from ...models import UniversalPayment, PaymentEvent
|
113
|
-
info.update({
|
114
|
-
'total_payments': UniversalPayment.objects.count(),
|
115
|
-
'total_events': PaymentEvent.objects.count(),
|
116
|
-
'providers_in_use': list(
|
117
|
-
UniversalPayment.objects.values_list('provider', flat=True).distinct()
|
118
|
-
),
|
119
|
-
})
|
120
|
-
except Exception:
|
121
|
-
info.update({
|
122
|
-
'total_payments': 'Unable to query',
|
123
|
-
'total_events': 'Unable to query',
|
124
|
-
'providers_in_use': [],
|
125
|
-
})
|
126
|
-
|
127
|
-
return info
|
128
|
-
|
129
|
-
def _get_test_scenarios(self):
|
130
|
-
"""Get available test scenarios."""
|
131
|
-
scenarios = [
|
132
|
-
{
|
133
|
-
'name': 'Template Component Test',
|
134
|
-
'description': 'Test all payment template components with sample data',
|
135
|
-
'endpoint': '/payments/test/?test=components',
|
136
|
-
'available': True,
|
137
|
-
},
|
138
|
-
{
|
139
|
-
'name': 'Status Badge Test',
|
140
|
-
'description': 'Test payment status badges for all possible statuses',
|
141
|
-
'endpoint': '/payments/test/?test=status_badges',
|
142
|
-
'available': True,
|
143
|
-
},
|
144
|
-
{
|
145
|
-
'name': 'Progress Bar Test',
|
146
|
-
'description': 'Test payment progress bars with different percentages',
|
147
|
-
'endpoint': '/payments/test/?test=progress_bars',
|
148
|
-
'available': True,
|
149
|
-
},
|
150
|
-
{
|
151
|
-
'name': 'Provider Statistics Test',
|
152
|
-
'description': 'Test provider statistics with sample data',
|
153
|
-
'endpoint': '/payments/test/?test=provider_stats',
|
154
|
-
'available': True,
|
155
|
-
},
|
156
|
-
{
|
157
|
-
'name': 'Real-time Updates Test',
|
158
|
-
'description': 'Test WebSocket connections and real-time updates',
|
159
|
-
'endpoint': '/payments/test/?test=realtime',
|
160
|
-
'available': False, # Requires WebSocket setup
|
161
|
-
},
|
162
|
-
{
|
163
|
-
'name': 'QR Code Generation Test',
|
164
|
-
'description': 'Test QR code generation for crypto payments',
|
165
|
-
'endpoint': '/payments/test/?test=qr_codes',
|
166
|
-
'available': True,
|
167
|
-
},
|
168
|
-
{
|
169
|
-
'name': 'API Integration Test',
|
170
|
-
'description': 'Test payment provider API integrations',
|
171
|
-
'endpoint': '/payments/test/?test=api_integration',
|
172
|
-
'available': False, # Requires API keys
|
173
|
-
},
|
174
|
-
]
|
175
|
-
|
176
|
-
# Add current test parameter
|
177
|
-
current_test = self.request.GET.get('test', 'overview')
|
178
|
-
for scenario in scenarios:
|
179
|
-
scenario['is_current'] = scenario['endpoint'].endswith(f'test={current_test}')
|
180
|
-
|
181
|
-
return scenarios
|
@@ -1,266 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Webhook processing views with signature validation.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import json
|
6
|
-
from django_cfg.modules.django_logger import get_logger
|
7
|
-
from typing import Dict, Any
|
8
|
-
|
9
|
-
from django.http import JsonResponse, HttpResponse
|
10
|
-
from django.views.decorators.csrf import csrf_exempt
|
11
|
-
from django.views.decorators.http import require_http_methods
|
12
|
-
from django.utils.decorators import method_decorator
|
13
|
-
from rest_framework.decorators import api_view, permission_classes
|
14
|
-
from rest_framework.permissions import AllowAny
|
15
|
-
from rest_framework.response import Response
|
16
|
-
from rest_framework import status
|
17
|
-
|
18
|
-
from ..services.core.payment_service import PaymentService
|
19
|
-
from ..tasks.webhook_processing import process_webhook_with_fallback
|
20
|
-
from ..services.security.webhook_validator import webhook_validator
|
21
|
-
from ..services.security.error_handler import error_handler, SecurityError, ValidationError
|
22
|
-
|
23
|
-
logger = get_logger("webhook_views")
|
24
|
-
|
25
|
-
|
26
|
-
@csrf_exempt
|
27
|
-
@require_http_methods(["POST"])
|
28
|
-
def webhook_handler(request, provider: str):
|
29
|
-
"""
|
30
|
-
Main webhook handler with signature validation.
|
31
|
-
|
32
|
-
Accepts webhooks from payment providers and processes them
|
33
|
-
with proper validation and fallback mechanisms.
|
34
|
-
"""
|
35
|
-
try:
|
36
|
-
# Parse webhook data
|
37
|
-
webhook_data = json.loads(request.body.decode('utf-8'))
|
38
|
-
|
39
|
-
# Extract request headers
|
40
|
-
request_headers = {
|
41
|
-
key: value for key, value in request.META.items()
|
42
|
-
if key.startswith('HTTP_')
|
43
|
-
}
|
44
|
-
|
45
|
-
# Generate idempotency key for deduplication
|
46
|
-
idempotency_key = _generate_idempotency_key(provider, webhook_data, request_headers)
|
47
|
-
|
48
|
-
logger.info(f"๐ฅ Received webhook from {provider}, key: {idempotency_key}")
|
49
|
-
|
50
|
-
# Validate webhook with enhanced security
|
51
|
-
is_valid, validation_error = webhook_validator.validate_webhook(
|
52
|
-
provider=provider,
|
53
|
-
webhook_data=webhook_data,
|
54
|
-
request_headers=request_headers,
|
55
|
-
raw_body=request.body
|
56
|
-
)
|
57
|
-
|
58
|
-
if not is_valid:
|
59
|
-
security_error = SecurityError(
|
60
|
-
f"Webhook validation failed: {validation_error}",
|
61
|
-
details={'provider': provider, 'validation_error': validation_error}
|
62
|
-
)
|
63
|
-
error_handler.handle_error(security_error, {
|
64
|
-
'provider': provider,
|
65
|
-
'webhook_data_keys': list(webhook_data.keys()),
|
66
|
-
'headers_count': len(request_headers)
|
67
|
-
}, request)
|
68
|
-
|
69
|
-
return JsonResponse(
|
70
|
-
{'error': 'Webhook validation failed', 'code': 'INVALID_WEBHOOK'},
|
71
|
-
status=403
|
72
|
-
)
|
73
|
-
|
74
|
-
# Process webhook (async with fallback to sync)
|
75
|
-
result = process_webhook_with_fallback(
|
76
|
-
provider=provider,
|
77
|
-
webhook_data=webhook_data,
|
78
|
-
idempotency_key=idempotency_key,
|
79
|
-
request_headers=request_headers
|
80
|
-
)
|
81
|
-
|
82
|
-
if result.get('success'):
|
83
|
-
logger.info(f"โ
Webhook processed successfully: {idempotency_key}")
|
84
|
-
return JsonResponse({
|
85
|
-
'status': 'success',
|
86
|
-
'idempotency_key': idempotency_key,
|
87
|
-
'processing_mode': result.get('mode', 'unknown')
|
88
|
-
})
|
89
|
-
else:
|
90
|
-
logger.error(f"โ Webhook processing failed: {result.get('error')}")
|
91
|
-
return JsonResponse({
|
92
|
-
'status': 'error',
|
93
|
-
'error': result.get('error', 'Processing failed'),
|
94
|
-
'idempotency_key': idempotency_key
|
95
|
-
}, status=400)
|
96
|
-
|
97
|
-
except json.JSONDecodeError as e:
|
98
|
-
validation_error = ValidationError(
|
99
|
-
f"Invalid JSON in webhook from {provider}",
|
100
|
-
details={'provider': provider, 'json_error': str(e)}
|
101
|
-
)
|
102
|
-
error_result = error_handler.handle_error(validation_error, {
|
103
|
-
'provider': provider,
|
104
|
-
'raw_body_length': len(request.body) if request.body else 0
|
105
|
-
}, request)
|
106
|
-
|
107
|
-
return JsonResponse({
|
108
|
-
'error': 'Invalid JSON',
|
109
|
-
'code': validation_error.error_code
|
110
|
-
}, status=400)
|
111
|
-
|
112
|
-
except Exception as e:
|
113
|
-
# Handle unexpected errors with centralized error handler
|
114
|
-
error_result = error_handler.handle_error(e, {
|
115
|
-
'provider': provider,
|
116
|
-
'operation': 'webhook_processing',
|
117
|
-
'webhook_data_available': 'webhook_data' in locals()
|
118
|
-
}, request)
|
119
|
-
|
120
|
-
return JsonResponse({
|
121
|
-
'error': 'Internal server error',
|
122
|
-
'code': error_result.error.error_code
|
123
|
-
}, status=500)
|
124
|
-
|
125
|
-
|
126
|
-
@api_view(['POST'])
|
127
|
-
@permission_classes([AllowAny])
|
128
|
-
def webhook_test(request):
|
129
|
-
"""
|
130
|
-
Test webhook endpoint for development.
|
131
|
-
|
132
|
-
Allows testing webhook processing without requiring
|
133
|
-
actual payment provider signatures.
|
134
|
-
"""
|
135
|
-
try:
|
136
|
-
provider = request.data.get('provider', 'test')
|
137
|
-
webhook_data = request.data.get('webhook_data', {})
|
138
|
-
|
139
|
-
# Add test marker
|
140
|
-
webhook_data['_test_webhook'] = True
|
141
|
-
|
142
|
-
# Generate test idempotency key
|
143
|
-
import uuid
|
144
|
-
idempotency_key = f"test_{uuid.uuid4().hex[:8]}"
|
145
|
-
|
146
|
-
logger.info(f"๐งช Processing test webhook: {provider}")
|
147
|
-
|
148
|
-
# Process with PaymentService directly (sync)
|
149
|
-
payment_service = PaymentService()
|
150
|
-
result = payment_service.process_webhook(
|
151
|
-
provider=provider,
|
152
|
-
webhook_data=webhook_data,
|
153
|
-
request_headers={'HTTP_X_TEST': 'true'}
|
154
|
-
)
|
155
|
-
|
156
|
-
return Response({
|
157
|
-
'status': 'success',
|
158
|
-
'test_mode': True,
|
159
|
-
'provider': provider,
|
160
|
-
'idempotency_key': idempotency_key,
|
161
|
-
'result': result.dict() if hasattr(result, 'dict') else result
|
162
|
-
})
|
163
|
-
|
164
|
-
except Exception as e:
|
165
|
-
logger.error(f"โ Test webhook error: {e}")
|
166
|
-
return Response({
|
167
|
-
'status': 'error',
|
168
|
-
'error': str(e),
|
169
|
-
'test_mode': True
|
170
|
-
}, status=status.HTTP_400_BAD_REQUEST)
|
171
|
-
|
172
|
-
|
173
|
-
def _validate_webhook_signature(provider: str, webhook_data: Dict[str, Any],
|
174
|
-
request_headers: Dict[str, str]) -> bool:
|
175
|
-
"""
|
176
|
-
Validate webhook signature based on provider.
|
177
|
-
|
178
|
-
Each provider has different signature validation methods.
|
179
|
-
"""
|
180
|
-
try:
|
181
|
-
if provider == 'nowpayments':
|
182
|
-
return _validate_nowpayments_signature(webhook_data, request_headers)
|
183
|
-
elif provider == 'cryptapi':
|
184
|
-
return _validate_cryptapi_signature(webhook_data, request_headers)
|
185
|
-
elif provider == 'test':
|
186
|
-
return True # Allow test webhooks
|
187
|
-
else:
|
188
|
-
logger.warning(f"Unknown provider for signature validation: {provider}")
|
189
|
-
return False
|
190
|
-
|
191
|
-
except Exception as e:
|
192
|
-
logger.error(f"Signature validation error for {provider}: {e}")
|
193
|
-
return False
|
194
|
-
|
195
|
-
|
196
|
-
def _validate_nowpayments_signature(webhook_data: Dict[str, Any],
|
197
|
-
request_headers: Dict[str, str]) -> bool:
|
198
|
-
"""Validate NowPayments webhook signature."""
|
199
|
-
import hmac
|
200
|
-
import hashlib
|
201
|
-
from ..utils.config_utils import get_payments_config
|
202
|
-
|
203
|
-
# Get IPN secret from config
|
204
|
-
config = get_payments_config()
|
205
|
-
if not config or not hasattr(config, 'providers') or 'nowpayments' not in config.providers:
|
206
|
-
logger.warning("NowPayments IPN secret not configured, skipping validation")
|
207
|
-
return True # Allow if not configured (development mode)
|
208
|
-
|
209
|
-
nowpayments_config = config.providers['nowpayments']
|
210
|
-
ipn_secret = getattr(nowpayments_config, 'ipn_secret', None)
|
211
|
-
|
212
|
-
if not ipn_secret:
|
213
|
-
logger.warning("NowPayments IPN secret not configured, skipping validation")
|
214
|
-
return True
|
215
|
-
|
216
|
-
# Get signature from headers
|
217
|
-
signature = request_headers.get('HTTP_X_NOWPAYMENTS_SIG')
|
218
|
-
if not signature:
|
219
|
-
logger.warning("No NowPayments signature found in headers")
|
220
|
-
return False
|
221
|
-
|
222
|
-
# Calculate expected signature
|
223
|
-
payload = json.dumps(webhook_data, separators=(',', ':'), sort_keys=True)
|
224
|
-
expected_signature = hmac.new(
|
225
|
-
ipn_secret.encode(),
|
226
|
-
payload.encode(),
|
227
|
-
hashlib.sha512
|
228
|
-
).hexdigest()
|
229
|
-
|
230
|
-
return hmac.compare_digest(signature, expected_signature)
|
231
|
-
|
232
|
-
|
233
|
-
def _validate_cryptapi_signature(webhook_data: Dict[str, Any],
|
234
|
-
request_headers: Dict[str, str]) -> bool:
|
235
|
-
"""Validate CryptAPI webhook signature."""
|
236
|
-
# CryptAPI uses different validation method
|
237
|
-
# For now, implement basic validation
|
238
|
-
|
239
|
-
# Check if required fields are present
|
240
|
-
required_fields = ['address_in', 'address_out', 'txid_in', 'value_coin', 'coin', 'confirmations']
|
241
|
-
for field in required_fields:
|
242
|
-
if field not in webhook_data:
|
243
|
-
logger.warning(f"Missing required field in CryptAPI webhook: {field}")
|
244
|
-
return False
|
245
|
-
|
246
|
-
return True
|
247
|
-
|
248
|
-
|
249
|
-
def _generate_idempotency_key(provider: str, webhook_data: Dict[str, Any],
|
250
|
-
request_headers: Dict[str, str]) -> str:
|
251
|
-
"""Generate idempotency key for webhook deduplication."""
|
252
|
-
import hashlib
|
253
|
-
|
254
|
-
# Use provider + payment ID + timestamp for uniqueness
|
255
|
-
payment_id = (
|
256
|
-
webhook_data.get('payment_id') or
|
257
|
-
webhook_data.get('order_id') or
|
258
|
-
webhook_data.get('id') or
|
259
|
-
'unknown'
|
260
|
-
)
|
261
|
-
|
262
|
-
timestamp = webhook_data.get('created_at') or webhook_data.get('timestamp')
|
263
|
-
|
264
|
-
# Create hash from key components
|
265
|
-
key_data = f"{provider}:{payment_id}:{timestamp}"
|
266
|
-
return hashlib.md5(key_data.encode()).hexdigest()[:16]
|