django-cfg 1.2.29__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 -9
- 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 +600 -108
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +470 -64
- 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 +381 -0
- django_cfg/apps/payments/management/commands/manage_providers.py +408 -0
- 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 +343 -163
- django_cfg/apps/payments/middleware/usage_tracking.py +250 -238
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +16 -20
- 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 +207 -67
- 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 -284
- 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 +344 -468
- django_cfg/apps/payments/services/core/subscription_service.py +425 -484
- 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 +232 -71
- django_cfg/apps/payments/services/providers/nowpayments.py +404 -219
- django_cfg/apps/payments/services/providers/registry.py +429 -80
- 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 +211 -130
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +129 -98
- django_cfg/apps/payments/signals/subscription_signals.py +195 -143
- 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 +46 -47
- django_cfg/apps/payments/urls_admin.py +49 -0
- 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/apps/tasks/urls.py +0 -2
- django_cfg/apps/tasks/urls_admin.py +14 -0
- django_cfg/apps/urls.py +4 -4
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +75 -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 -498
- django_cfg/modules/django_currency/__init__.py +16 -11
- django_cfg/modules/django_currency/clients/__init__.py +4 -4
- django_cfg/modules/django_currency/clients/coinpaprika_client.py +289 -0
- django_cfg/modules/django_currency/clients/yahoo_client.py +157 -0
- django_cfg/modules/django_currency/core/__init__.py +1 -7
- django_cfg/modules/django_currency/core/converter.py +18 -23
- django_cfg/modules/django_currency/core/models.py +122 -11
- django_cfg/modules/django_currency/database/__init__.py +4 -4
- django_cfg/modules/django_currency/database/database_loader.py +190 -309
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +65 -12
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/admin/components/action_grid.html +9 -9
- django_cfg/templates/admin/components/metric_card.html +5 -5
- django_cfg/templates/admin/components/status_badge.html +2 -2
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +152 -24
- django_cfg/templates/admin/snippets/components/quick_actions.html +3 -3
- django_cfg/templates/admin/snippets/components/system_health.html +1 -1
- django_cfg/templates/admin/snippets/tabs/overview_tab.html +49 -52
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/METADATA +5 -4
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/RECORD +172 -182
- 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 -178
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -323
- django_cfg/apps/payments/management/commands/populate_currencies.py +0 -246
- django_cfg/apps/payments/management/commands/update_currencies.py +0 -336
- django_cfg/apps/payments/managers/__init__.py +0 -22
- 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 -83
- django_cfg/apps/payments/managers/payment_manager.py +0 -44
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -56
- 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 -55
- 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 -297
- 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 -222
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi.py +0 -273
- django_cfg/apps/payments/services/providers/cryptomus.py +0 -311
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -637
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -475
- django_cfg/apps/payments/services/validators/__init__.py +0 -8
- 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/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 -36
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -27
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -144
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -346
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/urls_templates.py +0 -52
- django_cfg/apps/payments/utils/__init__.py +0 -45
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -245
- 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 -62
- 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 -111
- 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 -312
- django_cfg/apps/payments/views/templates/base.py +0 -204
- 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 -164
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -240
- 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 -65
- django_cfg/core/integration.py +0 -160
- django_cfg/modules/django_currency/clients/coingecko_client.py +0 -257
- django_cfg/modules/django_currency/clients/yfinance_client.py +0 -246
- 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.29.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.29.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,232 @@
|
|
1
|
+
"""
|
2
|
+
Webhook types for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Pydantic models for webhook validation and processing.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from decimal import Decimal
|
8
|
+
from typing import Optional, Dict, Any, Literal
|
9
|
+
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
10
|
+
from datetime import datetime, timedelta
|
11
|
+
|
12
|
+
|
13
|
+
class WebhookData(BaseModel):
|
14
|
+
"""Base webhook data structure."""
|
15
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
16
|
+
|
17
|
+
provider: str = Field(description="Provider name")
|
18
|
+
payment_id: str = Field(description="Payment ID")
|
19
|
+
status: str = Field(description="Payment status")
|
20
|
+
timestamp: datetime = Field(description="Webhook timestamp")
|
21
|
+
signature: Optional[str] = Field(None, description="Webhook signature")
|
22
|
+
raw_payload: Dict[str, Any] = Field(description="Raw webhook payload")
|
23
|
+
|
24
|
+
|
25
|
+
class NowPaymentsWebhook(BaseModel):
|
26
|
+
"""
|
27
|
+
NowPayments webhook structure.
|
28
|
+
|
29
|
+
Based on NowPayments IPN (Instant Payment Notification) format.
|
30
|
+
"""
|
31
|
+
model_config = ConfigDict(validate_assignment=True, extra="allow")
|
32
|
+
|
33
|
+
# Required fields from NowPayments
|
34
|
+
payment_id: str = Field(description="NowPayments payment ID")
|
35
|
+
payment_status: Literal[
|
36
|
+
'waiting', 'confirming', 'confirmed', 'sending', 'partially_paid',
|
37
|
+
'finished', 'failed', 'refunded', 'expired'
|
38
|
+
] = Field(description="Payment status")
|
39
|
+
pay_address: str = Field(description="Payment address")
|
40
|
+
price_amount: Decimal = Field(description="Original price amount")
|
41
|
+
price_currency: str = Field(description="Original price currency (USD)")
|
42
|
+
pay_amount: Decimal = Field(description="Amount to pay in crypto")
|
43
|
+
pay_currency: str = Field(description="Cryptocurrency code")
|
44
|
+
order_id: Optional[str] = Field(None, description="Order ID")
|
45
|
+
order_description: Optional[str] = Field(None, description="Order description")
|
46
|
+
|
47
|
+
# Optional fields
|
48
|
+
actually_paid: Optional[Decimal] = Field(None, description="Actually paid amount")
|
49
|
+
outcome_amount: Optional[Decimal] = Field(None, description="Outcome amount")
|
50
|
+
outcome_currency: Optional[str] = Field(None, description="Outcome currency")
|
51
|
+
|
52
|
+
# Network information
|
53
|
+
network: Optional[str] = Field(None, description="Blockchain network")
|
54
|
+
txn_id: Optional[str] = Field(None, description="Transaction ID")
|
55
|
+
|
56
|
+
# Timestamps
|
57
|
+
created_at: Optional[datetime] = Field(None, description="Creation timestamp")
|
58
|
+
updated_at: Optional[datetime] = Field(None, description="Update timestamp")
|
59
|
+
|
60
|
+
# Additional data
|
61
|
+
purchase_id: Optional[str] = Field(None, description="Purchase ID")
|
62
|
+
smart_contract: Optional[str] = Field(None, description="Smart contract address")
|
63
|
+
burning_percent: Optional[str] = Field(None, description="Burning percentage")
|
64
|
+
|
65
|
+
@field_validator('payment_status')
|
66
|
+
@classmethod
|
67
|
+
def validate_status(cls, v: str) -> str:
|
68
|
+
"""Validate payment status."""
|
69
|
+
valid_statuses = [
|
70
|
+
'waiting', 'confirming', 'confirmed', 'sending', 'partially_paid',
|
71
|
+
'finished', 'failed', 'refunded', 'expired'
|
72
|
+
]
|
73
|
+
if v not in valid_statuses:
|
74
|
+
raise ValueError(f"Invalid payment status: {v}")
|
75
|
+
return v
|
76
|
+
|
77
|
+
@field_validator('price_currency')
|
78
|
+
@classmethod
|
79
|
+
def validate_price_currency(cls, v: str) -> str:
|
80
|
+
"""Validate price currency is USD."""
|
81
|
+
if v.upper() != 'USD':
|
82
|
+
raise ValueError("Price currency must be USD")
|
83
|
+
return v.upper()
|
84
|
+
|
85
|
+
def to_universal_status(self) -> str:
|
86
|
+
"""Convert NowPayments status to universal payment status."""
|
87
|
+
status_mapping = {
|
88
|
+
'waiting': 'pending',
|
89
|
+
'confirming': 'confirming',
|
90
|
+
'confirmed': 'confirmed',
|
91
|
+
'sending': 'processing',
|
92
|
+
'partially_paid': 'partial',
|
93
|
+
'finished': 'completed',
|
94
|
+
'failed': 'failed',
|
95
|
+
'refunded': 'refunded',
|
96
|
+
'expired': 'expired'
|
97
|
+
}
|
98
|
+
return status_mapping.get(self.payment_status, 'unknown')
|
99
|
+
|
100
|
+
def is_final_status(self) -> bool:
|
101
|
+
"""Check if payment status is final."""
|
102
|
+
final_statuses = ['finished', 'failed', 'refunded', 'expired']
|
103
|
+
return self.payment_status in final_statuses
|
104
|
+
|
105
|
+
def is_successful(self) -> bool:
|
106
|
+
"""Check if payment is successful."""
|
107
|
+
return self.payment_status == 'finished'
|
108
|
+
|
109
|
+
|
110
|
+
class WebhookProcessingResult(BaseModel):
|
111
|
+
"""Result of webhook processing."""
|
112
|
+
model_config = ConfigDict(validate_assignment=True)
|
113
|
+
|
114
|
+
success: bool = Field(description="Processing success")
|
115
|
+
webhook_id: Optional[str] = Field(None, description="Webhook ID")
|
116
|
+
payment_id: Optional[str] = Field(None, description="Related payment ID")
|
117
|
+
provider: str = Field(description="Provider name")
|
118
|
+
status_before: Optional[str] = Field(None, description="Status before processing")
|
119
|
+
status_after: Optional[str] = Field(None, description="Status after processing")
|
120
|
+
actions_taken: list[str] = Field(default_factory=list, description="Actions performed")
|
121
|
+
balance_updated: bool = Field(default=False, description="Whether balance was updated")
|
122
|
+
notifications_sent: list[str] = Field(default_factory=list, description="Notifications sent")
|
123
|
+
error_message: Optional[str] = Field(None, description="Error message if failed")
|
124
|
+
processing_time_ms: Optional[int] = Field(None, description="Processing time in milliseconds")
|
125
|
+
processed: bool = Field(default=False, description="Whether webhook was processed")
|
126
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Processing timestamp")
|
127
|
+
|
128
|
+
|
129
|
+
class WebhookValidationResult(BaseModel):
|
130
|
+
"""Result of webhook validation."""
|
131
|
+
model_config = ConfigDict(validate_assignment=True)
|
132
|
+
|
133
|
+
is_valid: bool = Field(description="Validation result")
|
134
|
+
provider: str = Field(description="Provider name")
|
135
|
+
signature_valid: Optional[bool] = Field(None, description="Signature validation result")
|
136
|
+
payload_valid: bool = Field(description="Payload validation result")
|
137
|
+
error_message: Optional[str] = Field(None, description="Validation error message")
|
138
|
+
parsed_data: Optional[Dict[str, Any]] = Field(None, description="Parsed webhook data")
|
139
|
+
|
140
|
+
|
141
|
+
class WebhookSignature(BaseModel):
|
142
|
+
"""Webhook signature validation data."""
|
143
|
+
model_config = ConfigDict(validate_assignment=True)
|
144
|
+
|
145
|
+
provider: str = Field(description="Provider name")
|
146
|
+
signature: str = Field(description="Webhook signature")
|
147
|
+
payload: str = Field(description="Raw payload string")
|
148
|
+
secret_key: str = Field(description="Secret key for validation")
|
149
|
+
algorithm: str = Field(default="sha512", description="Signature algorithm")
|
150
|
+
|
151
|
+
def validate_signature(self) -> bool:
|
152
|
+
"""Validate webhook signature."""
|
153
|
+
import hmac
|
154
|
+
import hashlib
|
155
|
+
|
156
|
+
if self.algorithm == "sha512":
|
157
|
+
expected = hmac.new(
|
158
|
+
self.secret_key.encode('utf-8'),
|
159
|
+
self.payload.encode('utf-8'),
|
160
|
+
hashlib.sha512
|
161
|
+
).hexdigest()
|
162
|
+
elif self.algorithm == "sha256":
|
163
|
+
expected = hmac.new(
|
164
|
+
self.secret_key.encode('utf-8'),
|
165
|
+
self.payload.encode('utf-8'),
|
166
|
+
hashlib.sha256
|
167
|
+
).hexdigest()
|
168
|
+
else:
|
169
|
+
raise ValueError(f"Unsupported algorithm: {self.algorithm}")
|
170
|
+
|
171
|
+
return hmac.compare_digest(expected, self.signature)
|
172
|
+
|
173
|
+
|
174
|
+
class WebhookRetry(BaseModel):
|
175
|
+
"""Webhook retry configuration."""
|
176
|
+
model_config = ConfigDict(validate_assignment=True)
|
177
|
+
|
178
|
+
webhook_id: str = Field(description="Webhook ID")
|
179
|
+
attempt_number: int = Field(description="Retry attempt number")
|
180
|
+
max_attempts: int = Field(default=3, description="Maximum retry attempts")
|
181
|
+
delay_seconds: int = Field(default=60, description="Delay between retries")
|
182
|
+
last_error: Optional[str] = Field(None, description="Last error message")
|
183
|
+
next_retry_at: Optional[datetime] = Field(None, description="Next retry timestamp")
|
184
|
+
|
185
|
+
def should_retry(self) -> bool:
|
186
|
+
"""Check if webhook should be retried."""
|
187
|
+
return self.attempt_number < self.max_attempts
|
188
|
+
|
189
|
+
def calculate_next_retry(self) -> datetime:
|
190
|
+
"""Calculate next retry timestamp with exponential backoff."""
|
191
|
+
import math
|
192
|
+
|
193
|
+
# Exponential backoff: delay * (2 ^ attempt_number)
|
194
|
+
delay = self.delay_seconds * (2 ** self.attempt_number)
|
195
|
+
# Cap at 1 hour
|
196
|
+
delay = min(delay, 3600)
|
197
|
+
|
198
|
+
return datetime.utcnow() + timedelta(seconds=delay)
|
199
|
+
|
200
|
+
|
201
|
+
class WebhookEvent(BaseModel):
|
202
|
+
"""Webhook event for logging and monitoring."""
|
203
|
+
model_config = ConfigDict(validate_assignment=True)
|
204
|
+
|
205
|
+
event_id: str = Field(description="Event ID")
|
206
|
+
webhook_id: str = Field(description="Webhook ID")
|
207
|
+
provider: str = Field(description="Provider name")
|
208
|
+
event_type: Literal['received', 'validated', 'processed', 'failed', 'retried'] = Field(
|
209
|
+
description="Event type"
|
210
|
+
)
|
211
|
+
payment_id: Optional[str] = Field(None, description="Related payment ID")
|
212
|
+
status: Optional[str] = Field(None, description="Payment status")
|
213
|
+
message: Optional[str] = Field(None, description="Event message")
|
214
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
|
215
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Event timestamp")
|
216
|
+
ip_address: Optional[str] = Field(None, description="Source IP address")
|
217
|
+
user_agent: Optional[str] = Field(None, description="User agent")
|
218
|
+
|
219
|
+
def to_log_entry(self) -> Dict[str, Any]:
|
220
|
+
"""Convert to structured log entry."""
|
221
|
+
return {
|
222
|
+
'event_id': self.event_id,
|
223
|
+
'webhook_id': self.webhook_id,
|
224
|
+
'provider': self.provider,
|
225
|
+
'event_type': self.event_type,
|
226
|
+
'payment_id': self.payment_id,
|
227
|
+
'status': self.status,
|
228
|
+
'message': self.message,
|
229
|
+
'timestamp': self.timestamp.isoformat(),
|
230
|
+
'ip_address': self.ip_address,
|
231
|
+
'metadata': self.metadata
|
232
|
+
}
|
@@ -1,13 +1,38 @@
|
|
1
1
|
"""
|
2
|
-
Universal Payment
|
2
|
+
Optimized Signals for the Universal Payment System v2.0.
|
3
3
|
|
4
|
-
|
4
|
+
Minimal signals that only handle:
|
5
|
+
- Cache invalidation
|
6
|
+
- Event notifications
|
7
|
+
- Audit logging
|
8
|
+
|
9
|
+
All business logic is in managers to avoid duplication.
|
5
10
|
"""
|
6
11
|
|
7
|
-
from .
|
8
|
-
from .
|
9
|
-
|
12
|
+
from django.apps import apps
|
13
|
+
from django_cfg.modules.django_logger import get_logger
|
14
|
+
|
15
|
+
logger = get_logger("payment_signals")
|
16
|
+
|
17
|
+
|
18
|
+
def register_signals():
|
19
|
+
"""
|
20
|
+
Register all payment system signals.
|
21
|
+
|
22
|
+
Called from apps.py when Django starts.
|
23
|
+
"""
|
24
|
+
try:
|
25
|
+
# Import signal modules to register them
|
26
|
+
from . import payment_signals
|
27
|
+
from . import balance_signals
|
28
|
+
from . import subscription_signals
|
29
|
+
from . import api_key_signals
|
30
|
+
|
31
|
+
logger.info("Payment signals registered successfully")
|
32
|
+
|
33
|
+
except ImportError as e:
|
34
|
+
logger.error(f"Failed to register payment signals: {e}")
|
35
|
+
|
10
36
|
|
11
|
-
|
12
|
-
|
13
|
-
]
|
37
|
+
# Auto-register signals when module is imported
|
38
|
+
register_signals()
|
@@ -1,160 +1,241 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
API Key Signals for the Universal Payment System v2.0.
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
Minimal signals focused on cache invalidation and security notifications.
|
5
|
+
Business logic stays in APIKeyManager.
|
6
6
|
"""
|
7
7
|
|
8
8
|
from django.db.models.signals import post_save, post_delete, pre_save
|
9
9
|
from django.dispatch import receiver
|
10
|
-
from django.
|
11
|
-
from django.db import transaction
|
10
|
+
from django.core.cache import cache
|
12
11
|
from django.utils import timezone
|
13
|
-
import logging
|
14
12
|
|
15
13
|
from ..models import APIKey
|
14
|
+
from django_cfg.modules.django_logger import get_logger
|
16
15
|
|
17
|
-
|
18
|
-
logger = logging.getLogger(__name__)
|
19
|
-
|
20
|
-
|
21
|
-
@receiver(post_save, sender=User)
|
22
|
-
def create_default_api_key(sender, instance, created, **kwargs):
|
23
|
-
"""
|
24
|
-
Automatically create default API key for new users.
|
25
|
-
This ensures every user can immediately start using the API.
|
26
|
-
"""
|
27
|
-
if created:
|
28
|
-
try:
|
29
|
-
with transaction.atomic():
|
30
|
-
import secrets
|
31
|
-
key_value = f"ak_{secrets.token_urlsafe(32)}"
|
32
|
-
|
33
|
-
api_key = APIKey.objects.create(
|
34
|
-
user=instance,
|
35
|
-
name="Default API Key",
|
36
|
-
key_value=key_value,
|
37
|
-
key_prefix=key_value[:8],
|
38
|
-
is_active=True
|
39
|
-
)
|
40
|
-
|
41
|
-
logger.info(
|
42
|
-
f"Created default API key for user {instance.email}: {api_key.key_prefix}***"
|
43
|
-
)
|
44
|
-
|
45
|
-
# Optional: Send welcome email with API key info
|
46
|
-
# This would be handled in custom project implementations
|
47
|
-
# from .tasks import send_api_key_welcome_email
|
48
|
-
# send_api_key_welcome_email.delay(instance.id, api_key.id)
|
49
|
-
|
50
|
-
except Exception as e:
|
51
|
-
logger.error(f"Failed to create default API key for user {instance.email}: {e}")
|
52
|
-
|
53
|
-
|
54
|
-
@receiver(post_save, sender=User)
|
55
|
-
def ensure_user_has_api_key(sender, instance, **kwargs):
|
56
|
-
"""
|
57
|
-
Ensure user always has at least one API key.
|
58
|
-
Creates one if user has no active keys.
|
59
|
-
"""
|
60
|
-
# Skip if this is a new user (handled by create_default_api_key)
|
61
|
-
if kwargs.get('created', False):
|
62
|
-
return
|
63
|
-
|
64
|
-
# Check if user has any active keys
|
65
|
-
if not APIKey.objects.filter(user=instance, is_active=True).exists():
|
66
|
-
try:
|
67
|
-
with transaction.atomic():
|
68
|
-
import secrets
|
69
|
-
key_value = f"ak_{secrets.token_urlsafe(32)}"
|
70
|
-
|
71
|
-
api_key = APIKey.objects.create(
|
72
|
-
user=instance,
|
73
|
-
name="Recovery API Key",
|
74
|
-
key_value=key_value,
|
75
|
-
key_prefix=key_value[:8],
|
76
|
-
is_active=True
|
77
|
-
)
|
78
|
-
logger.info(
|
79
|
-
f"Created recovery API key for user {instance.email}: {api_key.key_prefix}***"
|
80
|
-
)
|
81
|
-
except Exception as e:
|
82
|
-
logger.error(f"Failed to create recovery API key for user {instance.email}: {e}")
|
16
|
+
logger = get_logger("api_key_signals")
|
83
17
|
|
84
18
|
|
85
19
|
@receiver(pre_save, sender=APIKey)
|
86
|
-
def
|
87
|
-
"""Store original
|
20
|
+
def store_original_api_key_data(sender, instance: APIKey, **kwargs):
|
21
|
+
"""Store original API key data for change detection."""
|
88
22
|
if instance.pk:
|
89
23
|
try:
|
90
|
-
|
91
|
-
instance._original_is_active =
|
24
|
+
original = APIKey.objects.get(pk=instance.pk)
|
25
|
+
instance._original_is_active = original.is_active
|
26
|
+
instance._original_total_requests = original.total_requests
|
92
27
|
except APIKey.DoesNotExist:
|
93
28
|
instance._original_is_active = None
|
29
|
+
instance._original_total_requests = None
|
30
|
+
else:
|
31
|
+
instance._original_is_active = None
|
32
|
+
instance._original_total_requests = None
|
94
33
|
|
95
34
|
|
96
35
|
@receiver(post_save, sender=APIKey)
|
97
|
-
def
|
98
|
-
"""
|
36
|
+
def handle_api_key_changes(sender, instance: APIKey, created: bool, **kwargs):
|
37
|
+
"""
|
38
|
+
Handle API key changes - only cache clearing and security notifications.
|
39
|
+
|
40
|
+
Business logic (usage tracking, validation) stays in managers.
|
41
|
+
"""
|
99
42
|
if created:
|
100
|
-
logger.info(
|
101
|
-
|
102
|
-
|
43
|
+
logger.info(f"New API key created", extra={
|
44
|
+
'api_key_id': str(instance.id),
|
45
|
+
'user_id': instance.user.id,
|
46
|
+
'name': instance.name,
|
47
|
+
'expires_at': instance.expires_at.isoformat() if instance.expires_at else None
|
48
|
+
})
|
49
|
+
|
50
|
+
# Set creation notification in cache
|
51
|
+
cache.set(
|
52
|
+
f"api_key_created:{instance.user.id}:{instance.id}",
|
53
|
+
{
|
54
|
+
'api_key_id': str(instance.id),
|
55
|
+
'name': instance.name,
|
56
|
+
'timestamp': timezone.now().isoformat()
|
57
|
+
},
|
58
|
+
timeout=86400 # 24 hours
|
103
59
|
)
|
60
|
+
|
104
61
|
else:
|
105
|
-
# Check
|
62
|
+
# Check for status changes
|
106
63
|
if hasattr(instance, '_original_is_active'):
|
107
|
-
|
108
|
-
|
64
|
+
old_active = instance._original_is_active
|
65
|
+
new_active = instance.is_active
|
109
66
|
|
110
|
-
if
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
67
|
+
if old_active != new_active:
|
68
|
+
if new_active:
|
69
|
+
_handle_api_key_activated(instance)
|
70
|
+
else:
|
71
|
+
_handle_api_key_deactivated(instance)
|
72
|
+
|
73
|
+
# Check for usage increases (security monitoring)
|
74
|
+
if hasattr(instance, '_original_total_requests'):
|
75
|
+
old_requests = instance._original_total_requests or 0
|
76
|
+
new_requests = instance.total_requests
|
77
|
+
|
78
|
+
if new_requests > old_requests:
|
79
|
+
request_increase = new_requests - old_requests
|
80
|
+
|
81
|
+
# Log high usage increases (potential security concern)
|
82
|
+
if request_increase > 100: # More than 100 requests at once
|
83
|
+
logger.warning(f"High API key usage increase", extra={
|
84
|
+
'api_key_id': str(instance.id),
|
85
|
+
'user_id': instance.user.id,
|
86
|
+
'old_requests': old_requests,
|
87
|
+
'new_requests': new_requests,
|
88
|
+
'increase': request_increase
|
89
|
+
})
|
90
|
+
|
91
|
+
_handle_high_usage_alert(instance, request_increase)
|
92
|
+
|
93
|
+
# Clear API key-related caches
|
94
|
+
_clear_api_key_caches(instance)
|
127
95
|
|
128
96
|
|
129
97
|
@receiver(post_delete, sender=APIKey)
|
130
|
-
def
|
131
|
-
"""
|
132
|
-
logger.warning(
|
133
|
-
|
134
|
-
|
98
|
+
def handle_api_key_deletion(sender, instance: APIKey, **kwargs):
|
99
|
+
"""Handle API key deletion."""
|
100
|
+
logger.warning(f"API key deleted", extra={
|
101
|
+
'api_key_id': str(instance.id),
|
102
|
+
'user_id': instance.user.id,
|
103
|
+
'name': instance.name,
|
104
|
+
'total_requests': instance.total_requests,
|
105
|
+
'deletion_timestamp': timezone.now().isoformat()
|
106
|
+
})
|
107
|
+
|
108
|
+
# Set deletion notification in cache
|
109
|
+
cache.set(
|
110
|
+
f"api_key_deleted:{instance.user.id}:{instance.id}",
|
111
|
+
{
|
112
|
+
'api_key_id': str(instance.id),
|
113
|
+
'name': instance.name,
|
114
|
+
'total_requests': instance.total_requests,
|
115
|
+
'timestamp': timezone.now().isoformat()
|
116
|
+
},
|
117
|
+
timeout=86400 * 30 # 30 days for audit
|
135
118
|
)
|
136
|
-
|
137
|
-
|
138
|
-
@receiver(post_delete, sender=APIKey)
|
139
|
-
def ensure_user_has_remaining_key(sender, instance, **kwargs):
|
140
|
-
"""
|
141
|
-
Ensure user still has at least one API key after deletion.
|
142
|
-
Creates a new one if this was the last active key.
|
143
|
-
"""
|
144
|
-
user = instance.user
|
145
119
|
|
146
|
-
#
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
120
|
+
# Clear caches
|
121
|
+
_clear_api_key_caches(instance)
|
122
|
+
|
123
|
+
|
124
|
+
# Helper functions (notifications and security monitoring only)
|
125
|
+
|
126
|
+
def _handle_api_key_activated(api_key: APIKey):
|
127
|
+
"""Handle API key activation (notification only)."""
|
128
|
+
try:
|
129
|
+
logger.info(f"API key activated", extra={
|
130
|
+
'api_key_id': str(api_key.id),
|
131
|
+
'user_id': api_key.user.id,
|
132
|
+
'name': api_key.name
|
133
|
+
})
|
134
|
+
|
135
|
+
# Set activation notification in cache
|
136
|
+
cache.set(
|
137
|
+
f"api_key_activated:{api_key.user.id}:{api_key.id}",
|
138
|
+
{
|
139
|
+
'api_key_id': str(api_key.id),
|
140
|
+
'name': api_key.name,
|
141
|
+
'timestamp': timezone.now().isoformat()
|
142
|
+
},
|
143
|
+
timeout=86400 # 24 hours
|
144
|
+
)
|
145
|
+
|
146
|
+
except Exception as e:
|
147
|
+
logger.error(f"Failed to handle API key activation: {e}")
|
148
|
+
|
149
|
+
|
150
|
+
def _handle_api_key_deactivated(api_key: APIKey):
|
151
|
+
"""Handle API key deactivation (security notification)."""
|
152
|
+
try:
|
153
|
+
logger.warning(f"API key deactivated", extra={
|
154
|
+
'api_key_id': str(api_key.id),
|
155
|
+
'user_id': api_key.user.id,
|
156
|
+
'name': api_key.name,
|
157
|
+
'total_requests': api_key.total_requests
|
158
|
+
})
|
159
|
+
|
160
|
+
# Set deactivation notification in cache
|
161
|
+
cache.set(
|
162
|
+
f"api_key_deactivated:{api_key.user.id}:{api_key.id}",
|
163
|
+
{
|
164
|
+
'api_key_id': str(api_key.id),
|
165
|
+
'name': api_key.name,
|
166
|
+
'total_requests': api_key.total_requests,
|
167
|
+
'timestamp': timezone.now().isoformat()
|
168
|
+
},
|
169
|
+
timeout=86400 * 7 # 7 days
|
170
|
+
)
|
171
|
+
|
172
|
+
except Exception as e:
|
173
|
+
logger.error(f"Failed to handle API key deactivation: {e}")
|
174
|
+
|
175
|
+
|
176
|
+
def _handle_high_usage_alert(api_key: APIKey, request_increase: int):
|
177
|
+
"""Handle high usage alert (security monitoring)."""
|
178
|
+
try:
|
179
|
+
logger.warning(f"High API key usage detected", extra={
|
180
|
+
'api_key_id': str(api_key.id),
|
181
|
+
'user_id': api_key.user.id,
|
182
|
+
'request_increase': request_increase,
|
183
|
+
'total_requests': api_key.total_requests
|
184
|
+
})
|
185
|
+
|
186
|
+
# Set high usage alert in cache
|
187
|
+
cache.set(
|
188
|
+
f"high_usage_alert:{api_key.user.id}:{api_key.id}",
|
189
|
+
{
|
190
|
+
'api_key_id': str(api_key.id),
|
191
|
+
'request_increase': request_increase,
|
192
|
+
'total_requests': api_key.total_requests,
|
193
|
+
'timestamp': timezone.now().isoformat()
|
194
|
+
},
|
195
|
+
timeout=86400 # 24 hours
|
196
|
+
)
|
197
|
+
|
198
|
+
# Check if we should temporarily disable the key (security measure)
|
199
|
+
if request_increase > 1000: # More than 1000 requests at once
|
200
|
+
logger.critical(f"Extremely high API usage - potential abuse", extra={
|
201
|
+
'api_key_id': str(api_key.id),
|
202
|
+
'user_id': api_key.user.id,
|
203
|
+
'request_increase': request_increase
|
204
|
+
})
|
205
|
+
|
206
|
+
# Set critical alert flag
|
207
|
+
cache.set(
|
208
|
+
f"critical_usage_alert:{api_key.user.id}:{api_key.id}",
|
209
|
+
{
|
210
|
+
'api_key_id': str(api_key.id),
|
211
|
+
'request_increase': request_increase,
|
212
|
+
'timestamp': timezone.now().isoformat(),
|
213
|
+
'action_required': True
|
214
|
+
},
|
215
|
+
timeout=86400 * 7 # 7 days
|
216
|
+
)
|
217
|
+
|
218
|
+
except Exception as e:
|
219
|
+
logger.error(f"Failed to handle high usage alert: {e}")
|
220
|
+
|
221
|
+
|
222
|
+
def _clear_api_key_caches(api_key: APIKey):
|
223
|
+
"""Clear API key-related cache entries."""
|
224
|
+
try:
|
225
|
+
cache_keys = [
|
226
|
+
f"api_key_validation:{api_key.key[:10]}...", # Partial key for security
|
227
|
+
f"user_api_keys:{api_key.user.id}",
|
228
|
+
f"api_key_stats:{api_key.user.id}",
|
229
|
+
f"api_key_usage:{api_key.id}",
|
230
|
+
]
|
231
|
+
|
232
|
+
cache.delete_many(cache_keys)
|
233
|
+
|
234
|
+
logger.debug(f"Cleared API key caches", extra={
|
235
|
+
'api_key_id': str(api_key.id),
|
236
|
+
'user_id': api_key.user.id,
|
237
|
+
'cache_keys_cleared': len(cache_keys)
|
238
|
+
})
|
239
|
+
|
240
|
+
except Exception as e:
|
241
|
+
logger.warning(f"Failed to clear API key caches: {e}")
|