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,21 +1,28 @@
|
|
1
1
|
"""
|
2
|
-
Payment models for the
|
2
|
+
Payment models for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Core payment model with simplified architecture focused on NowPayments.
|
3
5
|
"""
|
4
6
|
|
7
|
+
from decimal import Decimal
|
5
8
|
from django.db import models
|
6
9
|
from django.contrib.auth import get_user_model
|
7
|
-
from django.core.validators import MinValueValidator
|
10
|
+
from django.core.validators import MinValueValidator, MaxValueValidator
|
8
11
|
from django.core.exceptions import ValidationError
|
9
12
|
from django.utils import timezone
|
10
13
|
from .base import UUIDTimestampedModel
|
14
|
+
from .currencies import Currency, Network
|
11
15
|
|
12
16
|
User = get_user_model()
|
13
17
|
|
14
18
|
|
15
|
-
|
16
|
-
|
17
19
|
class UniversalPayment(UUIDTimestampedModel):
|
18
|
-
"""
|
20
|
+
"""
|
21
|
+
Universal payment model supporting all providers.
|
22
|
+
|
23
|
+
Simplified v2.0 architecture focused on NowPayments with extensible design.
|
24
|
+
Uses float for USD amounts as per requirements for performance and API compatibility.
|
25
|
+
"""
|
19
26
|
|
20
27
|
class PaymentStatus(models.TextChoices):
|
21
28
|
PENDING = "pending", "Pending"
|
@@ -29,384 +36,327 @@ class UniversalPayment(UUIDTimestampedModel):
|
|
29
36
|
|
30
37
|
class PaymentProvider(models.TextChoices):
|
31
38
|
NOWPAYMENTS = "nowpayments", "NowPayments"
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
39
|
+
# Future providers can be added here
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
def get_crypto_providers(cls):
|
43
|
+
"""Get list of crypto provider values."""
|
44
|
+
return [cls.NOWPAYMENTS]
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def is_crypto_provider(cls, provider_name: str) -> bool:
|
48
|
+
"""Check if provider handles cryptocurrency."""
|
49
|
+
return provider_name in cls.get_crypto_providers()
|
36
50
|
|
51
|
+
# User and identification
|
37
52
|
user = models.ForeignKey(
|
38
53
|
User,
|
39
54
|
on_delete=models.CASCADE,
|
40
|
-
related_name='
|
41
|
-
help_text="User who
|
55
|
+
related_name='payments',
|
56
|
+
help_text="User who created this payment"
|
42
57
|
)
|
43
58
|
|
44
|
-
|
59
|
+
internal_payment_id = models.CharField(
|
60
|
+
max_length=100,
|
61
|
+
unique=True,
|
62
|
+
db_index=True,
|
63
|
+
help_text="Internal payment identifier"
|
64
|
+
)
|
65
|
+
|
66
|
+
# Financial information (USD as float per requirements)
|
45
67
|
amount_usd = models.FloatField(
|
46
|
-
validators=[MinValueValidator(1.0)],
|
47
|
-
help_text="Payment amount in USD"
|
68
|
+
validators=[MinValueValidator(1.0), MaxValueValidator(50000.0)],
|
69
|
+
help_text="Payment amount in USD (float for performance)"
|
48
70
|
)
|
49
|
-
|
50
|
-
|
51
|
-
|
71
|
+
|
72
|
+
# Cryptocurrency information
|
73
|
+
currency = models.ForeignKey(
|
74
|
+
Currency,
|
75
|
+
on_delete=models.PROTECT,
|
76
|
+
related_name='payments',
|
77
|
+
help_text="Payment currency"
|
52
78
|
)
|
53
79
|
|
54
|
-
|
55
|
-
|
80
|
+
network = models.ForeignKey(
|
81
|
+
Network,
|
82
|
+
on_delete=models.PROTECT,
|
83
|
+
related_name='payments',
|
56
84
|
null=True,
|
57
85
|
blank=True,
|
58
|
-
help_text="
|
86
|
+
help_text="Blockchain network (for crypto payments)"
|
59
87
|
)
|
60
|
-
|
61
|
-
|
88
|
+
|
89
|
+
# Crypto amounts use Decimal for precision
|
90
|
+
pay_amount = models.DecimalField(
|
91
|
+
max_digits=20,
|
92
|
+
decimal_places=8,
|
62
93
|
null=True,
|
63
94
|
blank=True,
|
64
|
-
help_text="
|
95
|
+
help_text="Amount to pay in cryptocurrency (Decimal for precision)"
|
96
|
+
)
|
97
|
+
|
98
|
+
actual_amount_usd = models.FloatField(
|
99
|
+
null=True,
|
100
|
+
blank=True,
|
101
|
+
help_text="Actual amount received in USD"
|
65
102
|
)
|
66
103
|
|
67
|
-
# Fee information
|
68
104
|
fee_amount_usd = models.FloatField(
|
69
105
|
null=True,
|
70
106
|
blank=True,
|
71
|
-
validators=[MinValueValidator(0.0)],
|
72
107
|
help_text="Fee amount in USD"
|
73
108
|
)
|
74
109
|
|
75
|
-
#
|
110
|
+
# Provider information
|
76
111
|
provider = models.CharField(
|
77
112
|
max_length=50,
|
78
113
|
choices=PaymentProvider.choices,
|
114
|
+
default=PaymentProvider.NOWPAYMENTS,
|
79
115
|
help_text="Payment provider"
|
80
116
|
)
|
81
|
-
status = models.CharField(
|
82
|
-
max_length=20,
|
83
|
-
choices=PaymentStatus.choices,
|
84
|
-
default=PaymentStatus.PENDING,
|
85
|
-
help_text="Payment status"
|
86
|
-
)
|
87
117
|
|
88
|
-
# Provider-specific fields
|
89
118
|
provider_payment_id = models.CharField(
|
90
119
|
max_length=255,
|
91
120
|
null=True,
|
92
121
|
blank=True,
|
93
|
-
|
122
|
+
db_index=True,
|
94
123
|
help_text="Provider's payment ID"
|
95
124
|
)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
125
|
+
|
126
|
+
# Payment details
|
127
|
+
status = models.CharField(
|
128
|
+
max_length=20,
|
129
|
+
choices=PaymentStatus.choices,
|
130
|
+
default=PaymentStatus.PENDING,
|
131
|
+
db_index=True,
|
132
|
+
help_text="Current payment status"
|
100
133
|
)
|
101
134
|
|
102
|
-
# Crypto payment specific
|
103
135
|
pay_address = models.CharField(
|
104
|
-
max_length=
|
136
|
+
max_length=255,
|
105
137
|
null=True,
|
106
138
|
blank=True,
|
107
139
|
help_text="Cryptocurrency payment address"
|
108
140
|
)
|
109
|
-
|
110
|
-
|
111
|
-
blank=True,
|
112
|
-
help_text="Amount to pay in cryptocurrency"
|
113
|
-
)
|
114
|
-
network = models.CharField(
|
115
|
-
max_length=50,
|
141
|
+
|
142
|
+
payment_url = models.URLField(
|
116
143
|
null=True,
|
117
144
|
blank=True,
|
118
|
-
help_text="
|
145
|
+
help_text="Payment page URL"
|
119
146
|
)
|
120
147
|
|
121
|
-
#
|
122
|
-
|
123
|
-
|
124
|
-
help_text="Payment description"
|
125
|
-
)
|
126
|
-
order_id = models.CharField(
|
127
|
-
max_length=255,
|
148
|
+
# Transaction information
|
149
|
+
transaction_hash = models.CharField(
|
150
|
+
max_length=256,
|
128
151
|
null=True,
|
129
152
|
blank=True,
|
130
|
-
|
131
|
-
|
132
|
-
metadata = models.JSONField(
|
133
|
-
default=dict,
|
134
|
-
help_text="Additional metadata"
|
153
|
+
db_index=True,
|
154
|
+
help_text="Blockchain transaction hash"
|
135
155
|
)
|
136
156
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
blank=True,
|
141
|
-
help_text="Raw webhook data from provider"
|
157
|
+
confirmations_count = models.PositiveIntegerField(
|
158
|
+
default=0,
|
159
|
+
help_text="Number of blockchain confirmations"
|
142
160
|
)
|
143
161
|
|
144
|
-
#
|
162
|
+
# Security and validation
|
145
163
|
security_nonce = models.CharField(
|
146
164
|
max_length=64,
|
147
165
|
null=True,
|
148
166
|
blank=True,
|
149
167
|
db_index=True,
|
150
|
-
help_text="Security nonce for
|
151
|
-
)
|
152
|
-
provider_callback_url = models.CharField(
|
153
|
-
max_length=512,
|
154
|
-
null=True,
|
155
|
-
blank=True,
|
156
|
-
help_text="Full callback URL with security parameters"
|
168
|
+
help_text="Security nonce for validation"
|
157
169
|
)
|
158
170
|
|
159
|
-
#
|
160
|
-
|
161
|
-
max_length=256,
|
162
|
-
null=True,
|
163
|
-
blank=True,
|
164
|
-
db_index=True,
|
165
|
-
help_text="Main transaction hash/ID (txid_in for CryptAPI, hash for Cryptomus)"
|
166
|
-
)
|
167
|
-
confirmation_hash = models.CharField(
|
168
|
-
max_length=256,
|
171
|
+
# Timestamps
|
172
|
+
expires_at = models.DateTimeField(
|
169
173
|
null=True,
|
170
174
|
blank=True,
|
171
|
-
help_text="
|
175
|
+
help_text="When this payment expires"
|
172
176
|
)
|
173
|
-
|
174
|
-
|
177
|
+
|
178
|
+
completed_at = models.DateTimeField(
|
175
179
|
null=True,
|
176
180
|
blank=True,
|
177
|
-
help_text="
|
181
|
+
help_text="When this payment was completed"
|
178
182
|
)
|
179
|
-
|
180
|
-
|
181
|
-
|
183
|
+
|
184
|
+
# Metadata and description
|
185
|
+
description = models.TextField(
|
182
186
|
blank=True,
|
183
|
-
help_text="
|
187
|
+
help_text="Payment description"
|
184
188
|
)
|
185
|
-
|
189
|
+
|
190
|
+
callback_url = models.URLField(
|
186
191
|
null=True,
|
187
192
|
blank=True,
|
188
|
-
help_text="
|
189
|
-
)
|
190
|
-
confirmations_count = models.PositiveIntegerField(
|
191
|
-
default=0,
|
192
|
-
help_text="Number of blockchain confirmations"
|
193
|
+
help_text="Success callback URL"
|
193
194
|
)
|
194
195
|
|
195
|
-
|
196
|
-
expires_at = models.DateTimeField(
|
196
|
+
cancel_url = models.URLField(
|
197
197
|
null=True,
|
198
198
|
blank=True,
|
199
|
-
help_text="
|
199
|
+
help_text="Cancellation URL"
|
200
200
|
)
|
201
|
-
|
202
|
-
|
201
|
+
|
202
|
+
# Structured metadata (validated by Pydantic in services)
|
203
|
+
provider_data = models.JSONField(
|
204
|
+
default=dict,
|
203
205
|
blank=True,
|
204
|
-
help_text="
|
206
|
+
help_text="Provider-specific data (validated by Pydantic)"
|
205
207
|
)
|
206
|
-
|
207
|
-
|
208
|
+
|
209
|
+
webhook_data = models.JSONField(
|
210
|
+
default=dict,
|
208
211
|
blank=True,
|
209
|
-
help_text="
|
212
|
+
help_text="Webhook data (validated by Pydantic)"
|
210
213
|
)
|
211
|
-
|
212
|
-
#
|
213
|
-
from
|
214
|
-
objects =
|
215
|
-
|
214
|
+
|
215
|
+
# Manager
|
216
|
+
from .managers.payment_managers import PaymentManager
|
217
|
+
objects = PaymentManager()
|
218
|
+
|
216
219
|
class Meta:
|
217
|
-
db_table = '
|
218
|
-
verbose_name =
|
219
|
-
verbose_name_plural =
|
220
|
+
db_table = 'payments_universal'
|
221
|
+
verbose_name = 'Universal Payment'
|
222
|
+
verbose_name_plural = 'Universal Payments'
|
223
|
+
ordering = ['-created_at']
|
220
224
|
indexes = [
|
221
225
|
models.Index(fields=['user', 'status']),
|
226
|
+
models.Index(fields=['provider', 'status']),
|
227
|
+
models.Index(fields=['status', 'created_at']),
|
222
228
|
models.Index(fields=['provider_payment_id']),
|
223
|
-
models.Index(fields=['internal_payment_id']),
|
224
|
-
models.Index(fields=['status']),
|
225
|
-
models.Index(fields=['provider']),
|
226
|
-
models.Index(fields=['currency_code']),
|
227
|
-
models.Index(fields=['created_at']),
|
228
|
-
models.Index(fields=['processed_at']),
|
229
|
-
# Universal crypto provider indexes
|
230
|
-
models.Index(fields=['security_nonce']),
|
231
229
|
models.Index(fields=['transaction_hash']),
|
232
|
-
models.Index(fields=['
|
233
|
-
models.Index(fields=['provider', 'status', 'confirmations_count']),
|
230
|
+
models.Index(fields=['expires_at']),
|
234
231
|
]
|
235
|
-
|
236
|
-
|
232
|
+
constraints = [
|
233
|
+
models.CheckConstraint(
|
234
|
+
check=models.Q(amount_usd__gte=1.0),
|
235
|
+
name='payments_min_amount_check'
|
236
|
+
),
|
237
|
+
models.CheckConstraint(
|
238
|
+
check=models.Q(amount_usd__lte=50000.0),
|
239
|
+
name='payments_max_amount_check'
|
240
|
+
),
|
241
|
+
]
|
242
|
+
|
237
243
|
def __str__(self):
|
238
|
-
return f"{self.
|
239
|
-
|
244
|
+
return f"Payment {self.internal_payment_id} - ${self.amount_usd:.2f} {self.currency.code}"
|
245
|
+
|
246
|
+
def save(self, *args, **kwargs):
|
247
|
+
"""Override save to generate internal payment ID."""
|
248
|
+
if not self.internal_payment_id:
|
249
|
+
# Generate internal payment ID
|
250
|
+
timestamp = timezone.now().strftime('%Y%m%d%H%M%S')
|
251
|
+
self.internal_payment_id = f"PAY_{timestamp}_{str(self.id)[:8]}"
|
252
|
+
|
253
|
+
super().save(*args, **kwargs)
|
254
|
+
|
255
|
+
def clean(self):
|
256
|
+
"""Model validation."""
|
257
|
+
# Crypto payments must have network
|
258
|
+
if self.currency and self.currency.is_crypto and not self.network:
|
259
|
+
raise ValidationError("Cryptocurrency payments must specify a network")
|
260
|
+
|
261
|
+
# Fiat payments should not have network
|
262
|
+
if self.currency and self.currency.is_fiat and self.network:
|
263
|
+
raise ValidationError("Fiat payments should not specify a network")
|
264
|
+
|
265
|
+
# Validate amount limits
|
266
|
+
if self.amount_usd and (self.amount_usd < 1.0 or self.amount_usd > 50000.0):
|
267
|
+
raise ValidationError("Payment amount must be between $1.00 and $50,000.00")
|
268
|
+
|
269
|
+
# Validate expiration
|
270
|
+
if self.expires_at and self.expires_at <= timezone.now():
|
271
|
+
raise ValidationError("Expiration time must be in the future")
|
272
|
+
|
273
|
+
# Status properties
|
240
274
|
@property
|
241
275
|
def is_pending(self) -> bool:
|
242
|
-
"""Check if payment is
|
243
|
-
return self.status
|
244
|
-
|
245
|
-
self.PaymentStatus.CONFIRMING,
|
246
|
-
self.PaymentStatus.CONFIRMED
|
247
|
-
]
|
248
|
-
|
276
|
+
"""Check if payment is pending."""
|
277
|
+
return self.status == self.PaymentStatus.PENDING
|
278
|
+
|
249
279
|
@property
|
250
280
|
def is_completed(self) -> bool:
|
251
281
|
"""Check if payment is completed."""
|
252
282
|
return self.status == self.PaymentStatus.COMPLETED
|
253
|
-
|
283
|
+
|
254
284
|
@property
|
255
285
|
def is_failed(self) -> bool:
|
256
286
|
"""Check if payment failed."""
|
257
|
-
return self.status in [
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
287
|
+
return self.status in [
|
288
|
+
self.PaymentStatus.FAILED,
|
289
|
+
self.PaymentStatus.EXPIRED,
|
290
|
+
self.PaymentStatus.CANCELLED
|
291
|
+
]
|
292
|
+
|
264
293
|
@property
|
265
|
-
def
|
266
|
-
"""Check if
|
267
|
-
|
268
|
-
|
294
|
+
def is_expired(self) -> bool:
|
295
|
+
"""Check if payment is expired."""
|
296
|
+
if not self.expires_at:
|
297
|
+
return False
|
298
|
+
return timezone.now() > self.expires_at
|
299
|
+
|
269
300
|
@property
|
270
|
-
def
|
271
|
-
"""Check if
|
272
|
-
|
273
|
-
|
274
|
-
|
301
|
+
def requires_confirmation(self) -> bool:
|
302
|
+
"""Check if payment requires blockchain confirmation."""
|
303
|
+
return self.status in [
|
304
|
+
self.PaymentStatus.CONFIRMING,
|
305
|
+
self.PaymentStatus.CONFIRMED
|
306
|
+
]
|
307
|
+
|
308
|
+
# Display properties
|
275
309
|
@property
|
276
|
-
def
|
277
|
-
"""
|
278
|
-
|
279
|
-
|
280
|
-
|
310
|
+
def status_color(self) -> str:
|
311
|
+
"""Get color for status display."""
|
312
|
+
colors = {
|
313
|
+
self.PaymentStatus.PENDING: 'warning',
|
314
|
+
self.PaymentStatus.CONFIRMING: 'info',
|
315
|
+
self.PaymentStatus.CONFIRMED: 'primary',
|
316
|
+
self.PaymentStatus.COMPLETED: 'success',
|
317
|
+
self.PaymentStatus.FAILED: 'danger',
|
318
|
+
self.PaymentStatus.EXPIRED: 'secondary',
|
319
|
+
self.PaymentStatus.CANCELLED: 'secondary',
|
320
|
+
self.PaymentStatus.REFUNDED: 'warning',
|
321
|
+
}
|
322
|
+
return colors.get(self.status, 'secondary')
|
323
|
+
|
281
324
|
@property
|
282
|
-
def
|
283
|
-
"""
|
284
|
-
return
|
285
|
-
|
325
|
+
def amount_display(self) -> str:
|
326
|
+
"""Formatted amount display."""
|
327
|
+
return f"${self.amount_usd:.2f} USD"
|
328
|
+
|
286
329
|
@property
|
287
|
-
def
|
288
|
-
"""
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
return f"https://api.qrserver.com/v1/create-qr-code/?size={size}x{size}&data={payment_url}"
|
302
|
-
return ""
|
303
|
-
|
304
|
-
def mark_as_processed(self):
|
305
|
-
"""Mark payment as processed."""
|
306
|
-
if not self.processed_at:
|
307
|
-
self.processed_at = timezone.now()
|
308
|
-
self.save(update_fields=['processed_at'])
|
309
|
-
|
310
|
-
def update_from_webhook(self, webhook_data: dict):
|
311
|
-
"""Update payment from provider webhook data."""
|
312
|
-
self.webhook_data = webhook_data
|
313
|
-
|
314
|
-
# Update status if provided
|
315
|
-
if 'payment_status' in webhook_data:
|
316
|
-
self.status = webhook_data['payment_status']
|
317
|
-
|
318
|
-
# Update payment details if provided
|
319
|
-
if 'pay_address' in webhook_data:
|
320
|
-
self.pay_address = webhook_data['pay_address']
|
321
|
-
|
322
|
-
if 'pay_amount' in webhook_data:
|
323
|
-
self.pay_amount = float(str(webhook_data['pay_amount']))
|
324
|
-
|
325
|
-
if 'payment_id' in webhook_data:
|
326
|
-
self.provider_payment_id = webhook_data['payment_id']
|
327
|
-
|
328
|
-
# Universal crypto provider webhook fields
|
329
|
-
# CryptAPI format
|
330
|
-
if 'txid_in' in webhook_data:
|
331
|
-
self.transaction_hash = webhook_data['txid_in']
|
332
|
-
if 'txid_out' in webhook_data:
|
333
|
-
self.confirmation_hash = webhook_data['txid_out']
|
334
|
-
if 'address_in' in webhook_data:
|
335
|
-
self.sender_address = webhook_data['address_in']
|
336
|
-
if 'address_out' in webhook_data:
|
337
|
-
self.receiver_address = webhook_data['address_out']
|
338
|
-
if 'value_coin' in webhook_data:
|
339
|
-
self.crypto_amount = float(str(webhook_data['value_coin']))
|
340
|
-
|
341
|
-
# Cryptomus format
|
342
|
-
if 'hash' in webhook_data:
|
343
|
-
self.transaction_hash = webhook_data['hash']
|
344
|
-
if 'from_address' in webhook_data:
|
345
|
-
self.sender_address = webhook_data['from_address']
|
346
|
-
if 'to_address' in webhook_data:
|
347
|
-
self.receiver_address = webhook_data['to_address']
|
348
|
-
if 'amount' in webhook_data and isinstance(webhook_data['amount'], (int, float, str)):
|
349
|
-
try:
|
350
|
-
self.crypto_amount = float(str(webhook_data['amount']))
|
351
|
-
except (ValueError, TypeError):
|
352
|
-
pass
|
353
|
-
|
354
|
-
# Universal confirmations field
|
355
|
-
if 'confirmations' in webhook_data:
|
356
|
-
self.confirmations_count = int(webhook_data['confirmations'])
|
357
|
-
|
358
|
-
self.save()
|
359
|
-
|
330
|
+
def crypto_amount_display(self) -> str:
|
331
|
+
"""Formatted crypto amount display."""
|
332
|
+
if not self.pay_amount:
|
333
|
+
return "N/A"
|
334
|
+
return f"{self.pay_amount:.8f} {self.currency.code}"
|
335
|
+
|
336
|
+
# Business logic methods
|
337
|
+
def can_be_cancelled(self) -> bool:
|
338
|
+
"""Check if payment can be cancelled."""
|
339
|
+
return self.status in [
|
340
|
+
self.PaymentStatus.PENDING,
|
341
|
+
self.PaymentStatus.CONFIRMING
|
342
|
+
]
|
343
|
+
|
360
344
|
def can_be_refunded(self) -> bool:
|
361
345
|
"""Check if payment can be refunded."""
|
362
|
-
return self.
|
363
|
-
|
364
|
-
def
|
365
|
-
"""
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
self.PaymentStatus.CONFIRMING: '#fd7e14',
|
380
|
-
self.PaymentStatus.CONFIRMED: '#20c997',
|
381
|
-
self.PaymentStatus.COMPLETED: '#198754',
|
382
|
-
self.PaymentStatus.FAILED: '#dc3545',
|
383
|
-
self.PaymentStatus.REFUNDED: '#6f42c1',
|
384
|
-
self.PaymentStatus.EXPIRED: '#dc3545',
|
385
|
-
self.PaymentStatus.CANCELLED: '#6c757d'
|
386
|
-
}
|
387
|
-
return status_colors.get(self.status, '#6c757d')
|
388
|
-
|
389
|
-
def clean(self):
|
390
|
-
"""Validate payment data."""
|
391
|
-
|
392
|
-
# Validate minimum amount
|
393
|
-
if self.amount_usd < 1.0:
|
394
|
-
raise ValidationError("Minimum payment amount is $1.00")
|
395
|
-
|
396
|
-
# Validate crypto address for crypto payments
|
397
|
-
if self.is_crypto_payment and self.status != self.PaymentStatus.PENDING:
|
398
|
-
if not self.pay_address:
|
399
|
-
raise ValidationError("Payment address is required for crypto payments")
|
400
|
-
|
401
|
-
def save(self, *args, **kwargs):
|
402
|
-
"""Override save to run validation."""
|
403
|
-
if self.currency_code:
|
404
|
-
self.currency_code = self.currency_code.upper()
|
405
|
-
|
406
|
-
# Generate internal payment ID if not set
|
407
|
-
if not self.internal_payment_id:
|
408
|
-
import uuid
|
409
|
-
self.internal_payment_id = f"pay_{str(uuid.uuid4())[:8]}"
|
410
|
-
|
411
|
-
self.clean()
|
412
|
-
super().save(*args, **kwargs)
|
346
|
+
return self.status == self.PaymentStatus.COMPLETED
|
347
|
+
|
348
|
+
def mark_completed(self, actual_amount_usd: float = None, transaction_hash: str = None):
|
349
|
+
"""Mark payment as completed (delegates to manager)."""
|
350
|
+
return self.__class__.objects.mark_payment_completed(
|
351
|
+
self, actual_amount_usd, transaction_hash
|
352
|
+
)
|
353
|
+
|
354
|
+
def mark_failed(self, reason: str = None, error_code: str = None):
|
355
|
+
"""Mark payment as failed (delegates to manager)."""
|
356
|
+
return self.__class__.objects.mark_payment_failed(
|
357
|
+
self, reason, error_code
|
358
|
+
)
|
359
|
+
|
360
|
+
def cancel(self, reason: str = None):
|
361
|
+
"""Cancel payment (delegates to manager)."""
|
362
|
+
return self.__class__.objects.cancel_payment(self, reason)
|