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.
Files changed (59) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/admin/site_admin.py +47 -8
  3. django_cfg/apps/maintenance/migrations/0003_cloudflaresite_include_subdomains_and_more.py +27 -0
  4. django_cfg/apps/maintenance/models/cloudflare_site.py +41 -0
  5. django_cfg/apps/maintenance/services/maintenance_service.py +121 -32
  6. django_cfg/apps/maintenance/services/site_sync_service.py +56 -11
  7. django_cfg/apps/newsletter/signals.py +9 -8
  8. django_cfg/apps/payments/__init__.py +8 -0
  9. django_cfg/apps/payments/apps.py +22 -0
  10. django_cfg/apps/payments/managers/__init__.py +22 -0
  11. django_cfg/apps/payments/managers/api_key_manager.py +35 -0
  12. django_cfg/apps/payments/managers/balance_manager.py +361 -0
  13. django_cfg/apps/payments/managers/currency_manager.py +32 -0
  14. django_cfg/apps/payments/managers/payment_manager.py +44 -0
  15. django_cfg/apps/payments/managers/subscription_manager.py +37 -0
  16. django_cfg/apps/payments/managers/tariff_manager.py +29 -0
  17. django_cfg/apps/payments/middleware/__init__.py +13 -0
  18. django_cfg/apps/payments/migrations/0001_initial.py +982 -0
  19. django_cfg/apps/payments/migrations/__init__.py +1 -0
  20. django_cfg/apps/payments/models/__init__.py +49 -0
  21. django_cfg/apps/payments/models/api_keys.py +96 -0
  22. django_cfg/apps/payments/models/balance.py +209 -0
  23. django_cfg/apps/payments/models/base.py +14 -0
  24. django_cfg/apps/payments/models/currencies.py +138 -0
  25. django_cfg/apps/payments/models/events.py +73 -0
  26. django_cfg/apps/payments/models/payments.py +301 -0
  27. django_cfg/apps/payments/models/subscriptions.py +270 -0
  28. django_cfg/apps/payments/models/tariffs.py +102 -0
  29. django_cfg/apps/payments/serializers/__init__.py +56 -0
  30. django_cfg/apps/payments/serializers/api_keys.py +51 -0
  31. django_cfg/apps/payments/serializers/balance.py +59 -0
  32. django_cfg/apps/payments/serializers/currencies.py +55 -0
  33. django_cfg/apps/payments/serializers/payments.py +62 -0
  34. django_cfg/apps/payments/serializers/subscriptions.py +71 -0
  35. django_cfg/apps/payments/serializers/tariffs.py +56 -0
  36. django_cfg/apps/payments/services/__init__.py +14 -0
  37. django_cfg/apps/payments/services/base.py +68 -0
  38. django_cfg/apps/payments/services/nowpayments.py +78 -0
  39. django_cfg/apps/payments/services/providers.py +77 -0
  40. django_cfg/apps/payments/services/redis_service.py +215 -0
  41. django_cfg/apps/payments/urls.py +78 -0
  42. django_cfg/apps/payments/views/__init__.py +62 -0
  43. django_cfg/apps/payments/views/api_key_views.py +164 -0
  44. django_cfg/apps/payments/views/balance_views.py +75 -0
  45. django_cfg/apps/payments/views/currency_views.py +111 -0
  46. django_cfg/apps/payments/views/payment_views.py +111 -0
  47. django_cfg/apps/payments/views/subscription_views.py +135 -0
  48. django_cfg/apps/payments/views/tariff_views.py +131 -0
  49. django_cfg/core/config.py +26 -1
  50. django_cfg/core/generation.py +2 -1
  51. django_cfg/management/commands/check_settings.py +54 -0
  52. django_cfg/models/revolution.py +14 -0
  53. django_cfg/modules/base.py +9 -0
  54. django_cfg/utils/smart_defaults.py +211 -85
  55. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/METADATA +1 -1
  56. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/RECORD +59 -17
  57. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/WHEEL +0 -0
  58. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/entry_points.txt +0 -0
  59. {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}"