django-cfg 1.2.20__py3-none-any.whl → 1.2.22__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/maintenance/admin/site_admin.py +47 -8
- django_cfg/apps/maintenance/migrations/0003_cloudflaresite_include_subdomains_and_more.py +27 -0
- django_cfg/apps/maintenance/models/cloudflare_site.py +41 -0
- django_cfg/apps/maintenance/services/maintenance_service.py +121 -32
- django_cfg/apps/maintenance/services/site_sync_service.py +56 -11
- django_cfg/apps/newsletter/signals.py +9 -8
- django_cfg/apps/payments/__init__.py +8 -0
- django_cfg/apps/payments/apps.py +22 -0
- django_cfg/apps/payments/managers/__init__.py +22 -0
- django_cfg/apps/payments/managers/api_key_manager.py +35 -0
- django_cfg/apps/payments/managers/balance_manager.py +361 -0
- django_cfg/apps/payments/managers/currency_manager.py +32 -0
- django_cfg/apps/payments/managers/payment_manager.py +44 -0
- django_cfg/apps/payments/managers/subscription_manager.py +37 -0
- django_cfg/apps/payments/managers/tariff_manager.py +29 -0
- django_cfg/apps/payments/middleware/__init__.py +13 -0
- django_cfg/apps/payments/migrations/0001_initial.py +982 -0
- django_cfg/apps/payments/migrations/__init__.py +1 -0
- django_cfg/apps/payments/models/__init__.py +49 -0
- django_cfg/apps/payments/models/api_keys.py +96 -0
- django_cfg/apps/payments/models/balance.py +209 -0
- django_cfg/apps/payments/models/base.py +14 -0
- django_cfg/apps/payments/models/currencies.py +138 -0
- django_cfg/apps/payments/models/events.py +73 -0
- django_cfg/apps/payments/models/payments.py +301 -0
- django_cfg/apps/payments/models/subscriptions.py +270 -0
- django_cfg/apps/payments/models/tariffs.py +102 -0
- django_cfg/apps/payments/serializers/__init__.py +56 -0
- django_cfg/apps/payments/serializers/api_keys.py +51 -0
- django_cfg/apps/payments/serializers/balance.py +59 -0
- django_cfg/apps/payments/serializers/currencies.py +55 -0
- django_cfg/apps/payments/serializers/payments.py +62 -0
- django_cfg/apps/payments/serializers/subscriptions.py +71 -0
- django_cfg/apps/payments/serializers/tariffs.py +56 -0
- django_cfg/apps/payments/services/__init__.py +14 -0
- django_cfg/apps/payments/services/base.py +68 -0
- django_cfg/apps/payments/services/nowpayments.py +78 -0
- django_cfg/apps/payments/services/providers.py +77 -0
- django_cfg/apps/payments/services/redis_service.py +215 -0
- django_cfg/apps/payments/urls.py +78 -0
- django_cfg/apps/payments/views/__init__.py +62 -0
- django_cfg/apps/payments/views/api_key_views.py +164 -0
- django_cfg/apps/payments/views/balance_views.py +75 -0
- django_cfg/apps/payments/views/currency_views.py +111 -0
- django_cfg/apps/payments/views/payment_views.py +111 -0
- django_cfg/apps/payments/views/subscription_views.py +135 -0
- django_cfg/apps/payments/views/tariff_views.py +131 -0
- django_cfg/core/config.py +26 -1
- django_cfg/core/generation.py +2 -1
- django_cfg/management/commands/check_settings.py +54 -0
- django_cfg/models/revolution.py +14 -0
- django_cfg/modules/base.py +9 -0
- django_cfg/utils/smart_defaults.py +211 -85
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/METADATA +1 -1
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/RECORD +59 -17
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
# Migrations package
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""
|
2
|
+
Universal payments models package.
|
3
|
+
|
4
|
+
Django ORM models for the universal payments system.
|
5
|
+
"""
|
6
|
+
|
7
|
+
# Base models
|
8
|
+
from .base import TimestampedModel
|
9
|
+
|
10
|
+
# Currency models
|
11
|
+
from .currencies import Currency, CurrencyNetwork
|
12
|
+
|
13
|
+
# Payment models
|
14
|
+
from .payments import UniversalPayment
|
15
|
+
|
16
|
+
# Balance models
|
17
|
+
from .balance import UserBalance, Transaction
|
18
|
+
|
19
|
+
# Subscription models
|
20
|
+
from .subscriptions import EndpointGroup, Subscription
|
21
|
+
|
22
|
+
# Tariff models
|
23
|
+
from .tariffs import Tariff, TariffEndpointGroup
|
24
|
+
|
25
|
+
# API Keys
|
26
|
+
from .api_keys import APIKey
|
27
|
+
|
28
|
+
# Event sourcing
|
29
|
+
from .events import PaymentEvent
|
30
|
+
|
31
|
+
__all__ = [
|
32
|
+
# Base
|
33
|
+
'TimestampedModel',
|
34
|
+
|
35
|
+
# Currencies
|
36
|
+
'Currency',
|
37
|
+
'CurrencyNetwork',
|
38
|
+
|
39
|
+
# Models
|
40
|
+
'UniversalPayment',
|
41
|
+
'UserBalance',
|
42
|
+
'Transaction',
|
43
|
+
'EndpointGroup',
|
44
|
+
'Subscription',
|
45
|
+
'Tariff',
|
46
|
+
'TariffEndpointGroup',
|
47
|
+
'APIKey',
|
48
|
+
'PaymentEvent',
|
49
|
+
]
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""
|
2
|
+
API key models for the universal payments system.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.db import models
|
6
|
+
from django.contrib.auth import get_user_model
|
7
|
+
from django.utils import timezone
|
8
|
+
from .base import TimestampedModel
|
9
|
+
|
10
|
+
User = get_user_model()
|
11
|
+
|
12
|
+
|
13
|
+
class APIKey(TimestampedModel):
|
14
|
+
"""API keys for authentication and usage tracking."""
|
15
|
+
|
16
|
+
user = models.ForeignKey(
|
17
|
+
User,
|
18
|
+
on_delete=models.CASCADE,
|
19
|
+
related_name='api_keys',
|
20
|
+
help_text="API key owner"
|
21
|
+
)
|
22
|
+
|
23
|
+
# Key details
|
24
|
+
name = models.CharField(
|
25
|
+
max_length=100,
|
26
|
+
help_text="Human-readable key name"
|
27
|
+
)
|
28
|
+
key_value = models.CharField(
|
29
|
+
max_length=255,
|
30
|
+
unique=True,
|
31
|
+
help_text="API key value (plain text)"
|
32
|
+
)
|
33
|
+
key_prefix = models.CharField(
|
34
|
+
max_length=20,
|
35
|
+
help_text="Key prefix for identification"
|
36
|
+
)
|
37
|
+
|
38
|
+
# Permissions
|
39
|
+
is_active = models.BooleanField(
|
40
|
+
default=True,
|
41
|
+
help_text="Is key active"
|
42
|
+
)
|
43
|
+
|
44
|
+
# Usage tracking
|
45
|
+
last_used = models.DateTimeField(
|
46
|
+
null=True,
|
47
|
+
blank=True,
|
48
|
+
help_text="Last usage timestamp"
|
49
|
+
)
|
50
|
+
usage_count = models.PositiveBigIntegerField(
|
51
|
+
default=0,
|
52
|
+
help_text="Total usage count"
|
53
|
+
)
|
54
|
+
|
55
|
+
# Lifecycle
|
56
|
+
expires_at = models.DateTimeField(
|
57
|
+
null=True,
|
58
|
+
blank=True,
|
59
|
+
help_text="Key expiration"
|
60
|
+
)
|
61
|
+
|
62
|
+
# Import and assign manager
|
63
|
+
from ..managers import APIKeyManager
|
64
|
+
objects = APIKeyManager()
|
65
|
+
|
66
|
+
class Meta:
|
67
|
+
db_table = 'api_keys'
|
68
|
+
verbose_name = "API Key"
|
69
|
+
verbose_name_plural = "API Keys"
|
70
|
+
indexes = [
|
71
|
+
models.Index(fields=['user', 'is_active']),
|
72
|
+
models.Index(fields=['key_value']),
|
73
|
+
models.Index(fields=['key_prefix']),
|
74
|
+
models.Index(fields=['last_used']),
|
75
|
+
models.Index(fields=['expires_at']),
|
76
|
+
]
|
77
|
+
ordering = ['-created_at']
|
78
|
+
|
79
|
+
def __str__(self):
|
80
|
+
return f"API Key: {self.name} ({self.key_prefix}***)"
|
81
|
+
|
82
|
+
def is_valid(self) -> bool:
|
83
|
+
"""Check if API key is valid."""
|
84
|
+
if not self.is_active:
|
85
|
+
return False
|
86
|
+
|
87
|
+
if self.expires_at and self.expires_at <= timezone.now():
|
88
|
+
return False
|
89
|
+
|
90
|
+
return True
|
91
|
+
|
92
|
+
def record_usage(self):
|
93
|
+
"""Record API key usage."""
|
94
|
+
self.usage_count += 1
|
95
|
+
self.last_used = timezone.now()
|
96
|
+
self.save(update_fields=['usage_count', 'last_used'])
|
@@ -0,0 +1,209 @@
|
|
1
|
+
"""
|
2
|
+
Balance and transaction models for the universal payments system.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.db import models, transaction
|
6
|
+
from django.contrib.auth import get_user_model
|
7
|
+
from django.core.validators import MinValueValidator
|
8
|
+
from django.core.exceptions import ValidationError
|
9
|
+
from django.utils import timezone
|
10
|
+
from .base import TimestampedModel
|
11
|
+
|
12
|
+
User = get_user_model()
|
13
|
+
|
14
|
+
|
15
|
+
class UserBalance(TimestampedModel):
|
16
|
+
"""User balance model for tracking USD funds."""
|
17
|
+
|
18
|
+
user = models.OneToOneField(
|
19
|
+
User,
|
20
|
+
on_delete=models.CASCADE,
|
21
|
+
related_name='balance',
|
22
|
+
help_text="User who owns this balance"
|
23
|
+
)
|
24
|
+
amount_usd = models.FloatField(
|
25
|
+
default=0.0,
|
26
|
+
validators=[MinValueValidator(0.0)],
|
27
|
+
help_text="Current balance in USD"
|
28
|
+
)
|
29
|
+
reserved_usd = models.FloatField(
|
30
|
+
default=0.0,
|
31
|
+
validators=[MinValueValidator(0.0)],
|
32
|
+
help_text="Reserved balance in USD (for pending transactions)"
|
33
|
+
)
|
34
|
+
total_earned = models.FloatField(
|
35
|
+
default=0.0,
|
36
|
+
validators=[MinValueValidator(0.0)],
|
37
|
+
help_text="Total amount earned (lifetime)"
|
38
|
+
)
|
39
|
+
total_spent = models.FloatField(
|
40
|
+
default=0.0,
|
41
|
+
validators=[MinValueValidator(0.0)],
|
42
|
+
help_text="Total amount spent (lifetime)"
|
43
|
+
)
|
44
|
+
last_transaction_at = models.DateTimeField(
|
45
|
+
null=True,
|
46
|
+
blank=True,
|
47
|
+
help_text="When the last transaction occurred"
|
48
|
+
)
|
49
|
+
|
50
|
+
# Import and assign manager
|
51
|
+
from ..managers import UserBalanceManager
|
52
|
+
objects = UserBalanceManager()
|
53
|
+
|
54
|
+
class Meta:
|
55
|
+
db_table = 'user_balances'
|
56
|
+
verbose_name = "User Balance"
|
57
|
+
verbose_name_plural = "User Balances"
|
58
|
+
indexes = [
|
59
|
+
models.Index(fields=['user']),
|
60
|
+
models.Index(fields=['amount_usd']),
|
61
|
+
models.Index(fields=['last_transaction_at']),
|
62
|
+
]
|
63
|
+
|
64
|
+
def __str__(self):
|
65
|
+
return f"{self.user.email} - ${self.amount_usd}"
|
66
|
+
|
67
|
+
@property
|
68
|
+
def total_balance(self) -> float:
|
69
|
+
"""Get total balance (available + reserved)."""
|
70
|
+
return self.amount_usd + self.reserved_usd
|
71
|
+
|
72
|
+
@property
|
73
|
+
def has_sufficient_funds(self) -> bool:
|
74
|
+
"""Check if user has sufficient available funds."""
|
75
|
+
return self.amount_usd > 0
|
76
|
+
|
77
|
+
def can_debit(self, amount: float) -> bool:
|
78
|
+
"""Check if user can be debited the specified amount."""
|
79
|
+
return self.amount_usd >= amount
|
80
|
+
|
81
|
+
def get_transaction_summary(self):
|
82
|
+
"""Get transaction summary for this user."""
|
83
|
+
transactions = self.user.transactions.all()
|
84
|
+
return {
|
85
|
+
'total_transactions': transactions.count(),
|
86
|
+
'total_payments': transactions.filter(transaction_type=Transaction.TransactionType.PAYMENT).count(),
|
87
|
+
'total_subscriptions': transactions.filter(transaction_type=Transaction.TransactionType.SUBSCRIPTION).count(),
|
88
|
+
'total_refunds': transactions.filter(transaction_type=Transaction.TransactionType.REFUND).count(),
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
class Transaction(TimestampedModel):
|
93
|
+
"""Transaction history model."""
|
94
|
+
|
95
|
+
class TransactionType(models.TextChoices):
|
96
|
+
PAYMENT = "payment", "Payment"
|
97
|
+
SUBSCRIPTION = "subscription", "Subscription"
|
98
|
+
REFUND = "refund", "Refund"
|
99
|
+
CREDIT = "credit", "Credit"
|
100
|
+
DEBIT = "debit", "Debit"
|
101
|
+
HOLD = "hold", "Hold"
|
102
|
+
RELEASE = "release", "Release"
|
103
|
+
FEE = "fee", "Fee"
|
104
|
+
ADJUSTMENT = "adjustment", "Adjustment"
|
105
|
+
|
106
|
+
user = models.ForeignKey(
|
107
|
+
User,
|
108
|
+
on_delete=models.CASCADE,
|
109
|
+
related_name='transactions',
|
110
|
+
help_text="User who made this transaction"
|
111
|
+
)
|
112
|
+
amount_usd = models.FloatField(
|
113
|
+
help_text="Transaction amount in USD (positive for credits, negative for debits)"
|
114
|
+
)
|
115
|
+
transaction_type = models.CharField(
|
116
|
+
max_length=20,
|
117
|
+
choices=TransactionType.choices,
|
118
|
+
help_text="Type of transaction"
|
119
|
+
)
|
120
|
+
description = models.TextField(
|
121
|
+
help_text="Human-readable description of the transaction"
|
122
|
+
)
|
123
|
+
balance_before = models.FloatField(
|
124
|
+
help_text="User balance before this transaction"
|
125
|
+
)
|
126
|
+
balance_after = models.FloatField(
|
127
|
+
help_text="User balance after this transaction"
|
128
|
+
)
|
129
|
+
|
130
|
+
# Related objects (nullable for flexibility)
|
131
|
+
from .payments import UniversalPayment
|
132
|
+
from .subscriptions import Subscription
|
133
|
+
payment = models.ForeignKey(
|
134
|
+
UniversalPayment,
|
135
|
+
on_delete=models.SET_NULL,
|
136
|
+
null=True,
|
137
|
+
blank=True,
|
138
|
+
related_name='transactions',
|
139
|
+
help_text="Related payment (if applicable)"
|
140
|
+
)
|
141
|
+
subscription = models.ForeignKey(
|
142
|
+
Subscription,
|
143
|
+
on_delete=models.SET_NULL,
|
144
|
+
null=True,
|
145
|
+
blank=True,
|
146
|
+
related_name='transactions',
|
147
|
+
help_text="Related subscription (if applicable)"
|
148
|
+
)
|
149
|
+
|
150
|
+
# Additional metadata
|
151
|
+
reference_id = models.CharField(
|
152
|
+
max_length=255,
|
153
|
+
null=True,
|
154
|
+
blank=True,
|
155
|
+
help_text="External reference ID"
|
156
|
+
)
|
157
|
+
metadata = models.JSONField(
|
158
|
+
default=dict,
|
159
|
+
help_text="Additional transaction metadata"
|
160
|
+
)
|
161
|
+
|
162
|
+
class Meta:
|
163
|
+
db_table = 'user_transactions'
|
164
|
+
verbose_name = "Transaction"
|
165
|
+
verbose_name_plural = "Transactions"
|
166
|
+
indexes = [
|
167
|
+
models.Index(fields=['user', 'created_at']),
|
168
|
+
models.Index(fields=['transaction_type']),
|
169
|
+
models.Index(fields=['amount_usd']),
|
170
|
+
models.Index(fields=['created_at']),
|
171
|
+
models.Index(fields=['reference_id']),
|
172
|
+
]
|
173
|
+
ordering = ['-created_at']
|
174
|
+
|
175
|
+
def __str__(self):
|
176
|
+
sign = "+" if self.amount_usd >= 0 else ""
|
177
|
+
return f"{self.user.email} - {sign}${self.amount_usd} ({self.get_transaction_type_display()})"
|
178
|
+
|
179
|
+
@property
|
180
|
+
def is_credit(self) -> bool:
|
181
|
+
"""Check if this is a credit transaction."""
|
182
|
+
return self.amount_usd > 0
|
183
|
+
|
184
|
+
@property
|
185
|
+
def is_debit(self) -> bool:
|
186
|
+
"""Check if this is a debit transaction."""
|
187
|
+
return self.amount_usd < 0
|
188
|
+
|
189
|
+
def clean(self):
|
190
|
+
"""Validate transaction data."""
|
191
|
+
|
192
|
+
# Validate balance calculation
|
193
|
+
expected_balance = self.balance_before + self.amount_usd
|
194
|
+
if abs(expected_balance - self.balance_after) > 0.01: # Allow for rounding
|
195
|
+
raise ValidationError(
|
196
|
+
f"Balance calculation error: {self.balance_before} + {self.amount_usd} != {self.balance_after}"
|
197
|
+
)
|
198
|
+
|
199
|
+
# Validate transaction type and amount sign
|
200
|
+
if self.transaction_type == self.TransactionType.PAYMENT and self.amount_usd <= 0:
|
201
|
+
raise ValidationError("Payment transactions must have positive amounts")
|
202
|
+
|
203
|
+
if self.transaction_type == self.TransactionType.SUBSCRIPTION and self.amount_usd >= 0:
|
204
|
+
raise ValidationError("Subscription transactions must have negative amounts")
|
205
|
+
|
206
|
+
def save(self, *args, **kwargs):
|
207
|
+
"""Override save to run validation."""
|
208
|
+
self.clean()
|
209
|
+
super().save(*args, **kwargs)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
Base model classes for the universal payments system.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.db import models
|
6
|
+
|
7
|
+
|
8
|
+
class TimestampedModel(models.Model):
|
9
|
+
"""Base model with automatic timestamps."""
|
10
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
11
|
+
updated_at = models.DateTimeField(auto_now=True)
|
12
|
+
|
13
|
+
class Meta:
|
14
|
+
abstract = True
|
@@ -0,0 +1,138 @@
|
|
1
|
+
"""
|
2
|
+
Currency models for the universal payments system.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.db import models
|
6
|
+
from .base import TimestampedModel
|
7
|
+
|
8
|
+
|
9
|
+
class Currency(TimestampedModel):
|
10
|
+
"""Supported currencies for payments."""
|
11
|
+
|
12
|
+
class CurrencyType(models.TextChoices):
|
13
|
+
FIAT = "fiat", "Fiat Currency"
|
14
|
+
CRYPTO = "crypto", "Cryptocurrency"
|
15
|
+
|
16
|
+
code = models.CharField(
|
17
|
+
max_length=10,
|
18
|
+
unique=True,
|
19
|
+
help_text="Currency code (e.g., USD, BTC, ETH)"
|
20
|
+
)
|
21
|
+
name = models.CharField(
|
22
|
+
max_length=100,
|
23
|
+
help_text="Full currency name"
|
24
|
+
)
|
25
|
+
symbol = models.CharField(
|
26
|
+
max_length=10,
|
27
|
+
help_text="Currency symbol (e.g., $, ₿, Ξ)"
|
28
|
+
)
|
29
|
+
currency_type = models.CharField(
|
30
|
+
max_length=10,
|
31
|
+
choices=CurrencyType.choices,
|
32
|
+
help_text="Type of currency"
|
33
|
+
)
|
34
|
+
decimal_places = models.PositiveSmallIntegerField(
|
35
|
+
default=2,
|
36
|
+
help_text="Number of decimal places for this currency"
|
37
|
+
)
|
38
|
+
is_active = models.BooleanField(
|
39
|
+
default=True,
|
40
|
+
help_text="Whether this currency is active for payments"
|
41
|
+
)
|
42
|
+
min_payment_amount = models.FloatField(
|
43
|
+
default=1.0,
|
44
|
+
help_text="Minimum payment amount for this currency"
|
45
|
+
)
|
46
|
+
|
47
|
+
# Exchange rate to USD (base currency)
|
48
|
+
usd_rate = models.FloatField(
|
49
|
+
default=1.0,
|
50
|
+
help_text="Exchange rate to USD (1 unit of this currency = X USD)"
|
51
|
+
)
|
52
|
+
rate_updated_at = models.DateTimeField(
|
53
|
+
null=True,
|
54
|
+
blank=True,
|
55
|
+
help_text="When the exchange rate was last updated"
|
56
|
+
)
|
57
|
+
|
58
|
+
# Import and assign manager
|
59
|
+
from ..managers import CurrencyManager
|
60
|
+
objects = CurrencyManager()
|
61
|
+
|
62
|
+
class Meta:
|
63
|
+
db_table = 'payment_currencies'
|
64
|
+
verbose_name = "Currency"
|
65
|
+
verbose_name_plural = "Currencies"
|
66
|
+
indexes = [
|
67
|
+
models.Index(fields=['code']),
|
68
|
+
models.Index(fields=['currency_type']),
|
69
|
+
models.Index(fields=['is_active']),
|
70
|
+
]
|
71
|
+
ordering = ['code']
|
72
|
+
|
73
|
+
def __str__(self):
|
74
|
+
return f"{self.code} - {self.name}"
|
75
|
+
|
76
|
+
@property
|
77
|
+
def is_fiat(self) -> bool:
|
78
|
+
"""Check if this is a fiat currency."""
|
79
|
+
return self.currency_type == self.CurrencyType.FIAT
|
80
|
+
|
81
|
+
@property
|
82
|
+
def is_crypto(self) -> bool:
|
83
|
+
"""Check if this is a cryptocurrency."""
|
84
|
+
return self.currency_type == self.CurrencyType.CRYPTO
|
85
|
+
|
86
|
+
def to_usd(self, amount: float) -> float:
|
87
|
+
"""Convert amount of this currency to USD."""
|
88
|
+
return amount * self.usd_rate
|
89
|
+
|
90
|
+
def from_usd(self, usd_amount: float) -> float:
|
91
|
+
"""Convert USD amount to this currency."""
|
92
|
+
if self.usd_rate == 0:
|
93
|
+
return 0
|
94
|
+
return usd_amount / self.usd_rate
|
95
|
+
|
96
|
+
|
97
|
+
class CurrencyNetwork(TimestampedModel):
|
98
|
+
"""Networks/blockchains for cryptocurrencies."""
|
99
|
+
|
100
|
+
currency = models.ForeignKey(
|
101
|
+
Currency,
|
102
|
+
on_delete=models.CASCADE,
|
103
|
+
related_name='networks',
|
104
|
+
help_text="Currency this network supports"
|
105
|
+
)
|
106
|
+
network_name = models.CharField(
|
107
|
+
max_length=50,
|
108
|
+
help_text="Network name (e.g., mainnet, polygon, bsc)"
|
109
|
+
)
|
110
|
+
network_code = models.CharField(
|
111
|
+
max_length=20,
|
112
|
+
help_text="Network code for API integration"
|
113
|
+
)
|
114
|
+
is_active = models.BooleanField(
|
115
|
+
default=True,
|
116
|
+
help_text="Whether this network is active"
|
117
|
+
)
|
118
|
+
confirmation_blocks = models.PositiveIntegerField(
|
119
|
+
default=1,
|
120
|
+
help_text="Number of confirmations required"
|
121
|
+
)
|
122
|
+
|
123
|
+
# Import and assign manager
|
124
|
+
from ..managers import CurrencyNetworkManager
|
125
|
+
objects = CurrencyNetworkManager()
|
126
|
+
|
127
|
+
class Meta:
|
128
|
+
db_table = 'payment_currency_networks'
|
129
|
+
verbose_name = "Currency Network"
|
130
|
+
verbose_name_plural = "Currency Networks"
|
131
|
+
unique_together = [['currency', 'network_code']]
|
132
|
+
indexes = [
|
133
|
+
models.Index(fields=['currency', 'is_active']),
|
134
|
+
models.Index(fields=['network_code']),
|
135
|
+
]
|
136
|
+
|
137
|
+
def __str__(self):
|
138
|
+
return f"{self.currency.code} - {self.network_name}"
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"""
|
2
|
+
Event sourcing models for the universal payments system.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.db import models
|
6
|
+
from .base import TimestampedModel
|
7
|
+
|
8
|
+
|
9
|
+
class PaymentEvent(TimestampedModel):
|
10
|
+
"""Event sourcing for payment operations - immutable audit trail."""
|
11
|
+
|
12
|
+
class EventType(models.TextChoices):
|
13
|
+
PAYMENT_CREATED = 'payment_created', 'Payment Created'
|
14
|
+
WEBHOOK_RECEIVED = 'webhook_received', 'Webhook Received'
|
15
|
+
WEBHOOK_PROCESSED = 'webhook_processed', 'Webhook Processed'
|
16
|
+
BALANCE_UPDATED = 'balance_updated', 'Balance Updated'
|
17
|
+
REFUND_PROCESSED = 'refund_processed', 'Refund Processed'
|
18
|
+
STATUS_CHANGED = 'status_changed', 'Status Changed'
|
19
|
+
ERROR_OCCURRED = 'error_occurred', 'Error Occurred'
|
20
|
+
|
21
|
+
# Event identification
|
22
|
+
payment_id = models.CharField(
|
23
|
+
max_length=255,
|
24
|
+
db_index=True,
|
25
|
+
help_text="Payment identifier"
|
26
|
+
)
|
27
|
+
event_type = models.CharField(
|
28
|
+
max_length=50,
|
29
|
+
choices=EventType.choices,
|
30
|
+
db_index=True,
|
31
|
+
help_text="Type of event"
|
32
|
+
)
|
33
|
+
sequence_number = models.PositiveBigIntegerField(
|
34
|
+
help_text="Sequential number per payment"
|
35
|
+
)
|
36
|
+
|
37
|
+
# Event data (JSON for flexibility)
|
38
|
+
event_data = models.JSONField(
|
39
|
+
help_text="Event data payload"
|
40
|
+
)
|
41
|
+
|
42
|
+
# Operational metadata
|
43
|
+
processed_by = models.CharField(
|
44
|
+
max_length=100,
|
45
|
+
help_text="Worker/server that processed this event"
|
46
|
+
)
|
47
|
+
correlation_id = models.CharField(
|
48
|
+
max_length=255,
|
49
|
+
null=True,
|
50
|
+
blank=True,
|
51
|
+
help_text="Correlation ID for tracing"
|
52
|
+
)
|
53
|
+
idempotency_key = models.CharField(
|
54
|
+
max_length=255,
|
55
|
+
unique=True,
|
56
|
+
help_text="Idempotency key to prevent duplicates"
|
57
|
+
)
|
58
|
+
|
59
|
+
class Meta:
|
60
|
+
db_table = 'payment_events'
|
61
|
+
verbose_name = "Payment Event"
|
62
|
+
verbose_name_plural = "Payment Events"
|
63
|
+
indexes = [
|
64
|
+
models.Index(fields=['payment_id', 'sequence_number']),
|
65
|
+
models.Index(fields=['event_type', 'created_at']),
|
66
|
+
models.Index(fields=['idempotency_key']),
|
67
|
+
models.Index(fields=['correlation_id']),
|
68
|
+
models.Index(fields=['created_at']),
|
69
|
+
]
|
70
|
+
ordering = ['sequence_number']
|
71
|
+
|
72
|
+
def __str__(self):
|
73
|
+
return f"Event {self.sequence_number}: {self.event_type} for {self.payment_id}"
|