django-cfg 1.2.21__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 (51) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/newsletter/signals.py +9 -8
  3. django_cfg/apps/payments/__init__.py +8 -0
  4. django_cfg/apps/payments/apps.py +22 -0
  5. django_cfg/apps/payments/managers/__init__.py +22 -0
  6. django_cfg/apps/payments/managers/api_key_manager.py +35 -0
  7. django_cfg/apps/payments/managers/balance_manager.py +361 -0
  8. django_cfg/apps/payments/managers/currency_manager.py +32 -0
  9. django_cfg/apps/payments/managers/payment_manager.py +44 -0
  10. django_cfg/apps/payments/managers/subscription_manager.py +37 -0
  11. django_cfg/apps/payments/managers/tariff_manager.py +29 -0
  12. django_cfg/apps/payments/middleware/__init__.py +13 -0
  13. django_cfg/apps/payments/migrations/0001_initial.py +982 -0
  14. django_cfg/apps/payments/migrations/__init__.py +1 -0
  15. django_cfg/apps/payments/models/__init__.py +49 -0
  16. django_cfg/apps/payments/models/api_keys.py +96 -0
  17. django_cfg/apps/payments/models/balance.py +209 -0
  18. django_cfg/apps/payments/models/base.py +14 -0
  19. django_cfg/apps/payments/models/currencies.py +138 -0
  20. django_cfg/apps/payments/models/events.py +73 -0
  21. django_cfg/apps/payments/models/payments.py +301 -0
  22. django_cfg/apps/payments/models/subscriptions.py +270 -0
  23. django_cfg/apps/payments/models/tariffs.py +102 -0
  24. django_cfg/apps/payments/serializers/__init__.py +56 -0
  25. django_cfg/apps/payments/serializers/api_keys.py +51 -0
  26. django_cfg/apps/payments/serializers/balance.py +59 -0
  27. django_cfg/apps/payments/serializers/currencies.py +55 -0
  28. django_cfg/apps/payments/serializers/payments.py +62 -0
  29. django_cfg/apps/payments/serializers/subscriptions.py +71 -0
  30. django_cfg/apps/payments/serializers/tariffs.py +56 -0
  31. django_cfg/apps/payments/services/__init__.py +14 -0
  32. django_cfg/apps/payments/services/base.py +68 -0
  33. django_cfg/apps/payments/services/nowpayments.py +78 -0
  34. django_cfg/apps/payments/services/providers.py +77 -0
  35. django_cfg/apps/payments/services/redis_service.py +215 -0
  36. django_cfg/apps/payments/urls.py +78 -0
  37. django_cfg/apps/payments/views/__init__.py +62 -0
  38. django_cfg/apps/payments/views/api_key_views.py +164 -0
  39. django_cfg/apps/payments/views/balance_views.py +75 -0
  40. django_cfg/apps/payments/views/currency_views.py +111 -0
  41. django_cfg/apps/payments/views/payment_views.py +111 -0
  42. django_cfg/apps/payments/views/subscription_views.py +135 -0
  43. django_cfg/apps/payments/views/tariff_views.py +131 -0
  44. django_cfg/core/config.py +6 -0
  45. django_cfg/models/revolution.py +14 -0
  46. django_cfg/modules/base.py +9 -0
  47. {django_cfg-1.2.21.dist-info → django_cfg-1.2.22.dist-info}/METADATA +1 -1
  48. {django_cfg-1.2.21.dist-info → django_cfg-1.2.22.dist-info}/RECORD +51 -10
  49. {django_cfg-1.2.21.dist-info → django_cfg-1.2.22.dist-info}/WHEEL +0 -0
  50. {django_cfg-1.2.21.dist-info → django_cfg-1.2.22.dist-info}/entry_points.txt +0 -0
  51. {django_cfg-1.2.21.dist-info → django_cfg-1.2.22.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,301 @@
1
+ """
2
+ Payment 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.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
+
16
+ class UniversalPayment(TimestampedModel):
17
+ """Universal payment model for all providers."""
18
+
19
+ class PaymentStatus(models.TextChoices):
20
+ PENDING = "pending", "Pending"
21
+ CONFIRMING = "confirming", "Confirming"
22
+ CONFIRMED = "confirmed", "Confirmed"
23
+ COMPLETED = "completed", "Completed"
24
+ FAILED = "failed", "Failed"
25
+ EXPIRED = "expired", "Expired"
26
+ CANCELLED = "cancelled", "Cancelled"
27
+ REFUNDED = "refunded", "Refunded"
28
+
29
+ class PaymentProvider(models.TextChoices):
30
+ NOWPAYMENTS = "nowpayments", "NowPayments"
31
+ STRIPE = "stripe", "Stripe"
32
+ INTERNAL = "internal", "Internal"
33
+
34
+ user = models.ForeignKey(
35
+ User,
36
+ on_delete=models.CASCADE,
37
+ related_name='universal_payments',
38
+ help_text="User who initiated this payment"
39
+ )
40
+
41
+ # Financial data
42
+ amount_usd = models.FloatField(
43
+ validators=[MinValueValidator(1.0)],
44
+ help_text="Payment amount in USD"
45
+ )
46
+ currency_code = models.CharField(
47
+ max_length=10,
48
+ help_text="Currency used for payment"
49
+ )
50
+
51
+ # Actual received amount (may differ from requested)
52
+ actual_amount_usd = models.FloatField(
53
+ null=True,
54
+ blank=True,
55
+ help_text="Actual received amount in USD"
56
+ )
57
+ actual_currency_code = models.CharField(
58
+ max_length=10,
59
+ null=True,
60
+ blank=True,
61
+ help_text="Actual received currency"
62
+ )
63
+
64
+ # Fee information
65
+ fee_amount_usd = models.FloatField(
66
+ null=True,
67
+ blank=True,
68
+ validators=[MinValueValidator(0.0)],
69
+ help_text="Fee amount in USD"
70
+ )
71
+
72
+ # Payment details
73
+ provider = models.CharField(
74
+ max_length=50,
75
+ choices=PaymentProvider.choices,
76
+ help_text="Payment provider"
77
+ )
78
+ status = models.CharField(
79
+ max_length=20,
80
+ choices=PaymentStatus.choices,
81
+ default=PaymentStatus.PENDING,
82
+ help_text="Payment status"
83
+ )
84
+
85
+ # Provider-specific fields
86
+ provider_payment_id = models.CharField(
87
+ max_length=255,
88
+ null=True,
89
+ blank=True,
90
+ unique=True,
91
+ help_text="Provider's payment ID"
92
+ )
93
+ internal_payment_id = models.CharField(
94
+ max_length=100,
95
+ unique=True,
96
+ help_text="Internal payment identifier"
97
+ )
98
+
99
+ # Crypto payment specific
100
+ pay_address = models.CharField(
101
+ max_length=200,
102
+ null=True,
103
+ blank=True,
104
+ help_text="Cryptocurrency payment address"
105
+ )
106
+ pay_amount = models.FloatField(
107
+ null=True,
108
+ blank=True,
109
+ help_text="Amount to pay in cryptocurrency"
110
+ )
111
+ network = models.CharField(
112
+ max_length=50,
113
+ null=True,
114
+ blank=True,
115
+ help_text="Blockchain network (mainnet, testnet, etc.)"
116
+ )
117
+
118
+ # Metadata
119
+ description = models.TextField(
120
+ blank=True,
121
+ help_text="Payment description"
122
+ )
123
+ order_id = models.CharField(
124
+ max_length=255,
125
+ null=True,
126
+ blank=True,
127
+ help_text="Order reference ID"
128
+ )
129
+ metadata = models.JSONField(
130
+ default=dict,
131
+ help_text="Additional metadata"
132
+ )
133
+
134
+ # Provider webhook data
135
+ webhook_data = models.JSONField(
136
+ null=True,
137
+ blank=True,
138
+ help_text="Raw webhook data from provider"
139
+ )
140
+
141
+ # Timestamps
142
+ expires_at = models.DateTimeField(
143
+ null=True,
144
+ blank=True,
145
+ help_text="Payment expiration time"
146
+ )
147
+ completed_at = models.DateTimeField(
148
+ null=True,
149
+ blank=True,
150
+ help_text="Payment completion time"
151
+ )
152
+ processed_at = models.DateTimeField(
153
+ null=True,
154
+ blank=True,
155
+ help_text="When the payment was processed and funds added to balance"
156
+ )
157
+
158
+ # Import and assign manager
159
+ from ..managers import UniversalPaymentManager
160
+ objects = UniversalPaymentManager()
161
+
162
+ class Meta:
163
+ db_table = 'universal_payments'
164
+ verbose_name = "Universal Payment"
165
+ verbose_name_plural = "Universal Payments"
166
+ indexes = [
167
+ models.Index(fields=['user', 'status']),
168
+ models.Index(fields=['provider_payment_id']),
169
+ models.Index(fields=['internal_payment_id']),
170
+ models.Index(fields=['status']),
171
+ models.Index(fields=['provider']),
172
+ models.Index(fields=['currency_code']),
173
+ models.Index(fields=['created_at']),
174
+ models.Index(fields=['processed_at']),
175
+ ]
176
+ ordering = ['-created_at']
177
+
178
+ def __str__(self):
179
+ return f"{self.user.email} - ${self.amount_usd} ({self.currency_code}) - {self.get_status_display()}"
180
+
181
+ @property
182
+ def is_pending(self) -> bool:
183
+ """Check if payment is still pending."""
184
+ return self.status in [
185
+ self.PaymentStatus.PENDING,
186
+ self.PaymentStatus.CONFIRMING,
187
+ self.PaymentStatus.CONFIRMED
188
+ ]
189
+
190
+ @property
191
+ def is_completed(self) -> bool:
192
+ """Check if payment is completed."""
193
+ return self.status == self.PaymentStatus.COMPLETED
194
+
195
+ @property
196
+ def is_failed(self) -> bool:
197
+ """Check if payment failed."""
198
+ return self.status in [self.PaymentStatus.FAILED, self.PaymentStatus.EXPIRED]
199
+
200
+ @property
201
+ def needs_processing(self) -> bool:
202
+ """Check if payment needs to be processed (completed but not processed)."""
203
+ return self.is_completed and not self.processed_at
204
+
205
+ @property
206
+ def is_crypto_payment(self) -> bool:
207
+ """Check if this is a cryptocurrency payment."""
208
+ return self.provider == self.PaymentProvider.NOWPAYMENTS
209
+
210
+ def get_payment_url(self) -> str:
211
+ """Get payment URL for QR code or direct payment."""
212
+ if self.pay_address and self.pay_amount:
213
+ return f"{self.currency_code.lower()}:{self.pay_address}?amount={self.pay_amount}"
214
+ return ""
215
+
216
+ def get_qr_code_url(self, size: int = 200) -> str:
217
+ """Get QR code URL for payment."""
218
+ payment_url = self.get_payment_url()
219
+ if payment_url:
220
+ return f"https://api.qrserver.com/v1/create-qr-code/?size={size}x{size}&data={payment_url}"
221
+ return ""
222
+
223
+ def mark_as_processed(self):
224
+ """Mark payment as processed."""
225
+ if not self.processed_at:
226
+ self.processed_at = timezone.now()
227
+ self.save(update_fields=['processed_at'])
228
+
229
+ def update_from_webhook(self, webhook_data: dict):
230
+ """Update payment from provider webhook data."""
231
+ self.webhook_data = webhook_data
232
+
233
+ # Update status if provided
234
+ if 'payment_status' in webhook_data:
235
+ self.status = webhook_data['payment_status']
236
+
237
+ # Update payment details if provided
238
+ if 'pay_address' in webhook_data:
239
+ self.pay_address = webhook_data['pay_address']
240
+
241
+ if 'pay_amount' in webhook_data:
242
+ self.pay_amount = float(str(webhook_data['pay_amount']))
243
+
244
+ if 'payment_id' in webhook_data:
245
+ self.provider_payment_id = webhook_data['payment_id']
246
+
247
+ self.save()
248
+
249
+ def can_be_refunded(self) -> bool:
250
+ """Check if payment can be refunded."""
251
+ return self.is_completed and self.processed_at
252
+
253
+ def get_currency_display_name(self) -> str:
254
+ """Get human-readable currency name."""
255
+ # This could be enhanced to lookup from Currency model
256
+ currency_names = {
257
+ 'BTC': 'Bitcoin',
258
+ 'ETH': 'Ethereum',
259
+ 'USD': 'US Dollar',
260
+ 'EUR': 'Euro',
261
+ }
262
+ return currency_names.get(self.currency_code, self.currency_code)
263
+
264
+ def get_status_color(self) -> str:
265
+ """Get color for status display."""
266
+ status_colors = {
267
+ self.PaymentStatus.PENDING: '#6c757d',
268
+ self.PaymentStatus.CONFIRMING: '#fd7e14',
269
+ self.PaymentStatus.CONFIRMED: '#20c997',
270
+ self.PaymentStatus.COMPLETED: '#198754',
271
+ self.PaymentStatus.FAILED: '#dc3545',
272
+ self.PaymentStatus.REFUNDED: '#6f42c1',
273
+ self.PaymentStatus.EXPIRED: '#dc3545',
274
+ self.PaymentStatus.CANCELLED: '#6c757d'
275
+ }
276
+ return status_colors.get(self.status, '#6c757d')
277
+
278
+ def clean(self):
279
+ """Validate payment data."""
280
+
281
+ # Validate minimum amount
282
+ if self.amount_usd < 1.0:
283
+ raise ValidationError("Minimum payment amount is $1.00")
284
+
285
+ # Validate crypto address for crypto payments
286
+ if self.is_crypto_payment and self.status != self.PaymentStatus.PENDING:
287
+ if not self.pay_address:
288
+ raise ValidationError("Payment address is required for crypto payments")
289
+
290
+ def save(self, *args, **kwargs):
291
+ """Override save to run validation."""
292
+ if self.currency_code:
293
+ self.currency_code = self.currency_code.upper()
294
+
295
+ # Generate internal payment ID if not set
296
+ if not self.internal_payment_id:
297
+ import uuid
298
+ self.internal_payment_id = f"pay_{str(uuid.uuid4())[:8]}"
299
+
300
+ self.clean()
301
+ super().save(*args, **kwargs)
@@ -0,0 +1,270 @@
1
+ """
2
+ Subscription 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.core.validators import MinValueValidator
8
+ from django.utils import timezone
9
+ from datetime import timedelta
10
+ from .base import TimestampedModel
11
+
12
+ User = get_user_model()
13
+
14
+
15
+ class EndpointGroup(TimestampedModel):
16
+ """API endpoint groups for subscription management."""
17
+
18
+ name = models.CharField(
19
+ max_length=100,
20
+ unique=True,
21
+ help_text="Endpoint group name"
22
+ )
23
+ display_name = models.CharField(
24
+ max_length=200,
25
+ help_text="Human-readable name"
26
+ )
27
+ description = models.TextField(
28
+ blank=True,
29
+ help_text="Group description"
30
+ )
31
+
32
+ # Pricing tiers
33
+ basic_price = models.FloatField(
34
+ default=0.0,
35
+ validators=[MinValueValidator(0.0)],
36
+ help_text="Basic tier monthly price"
37
+ )
38
+ premium_price = models.FloatField(
39
+ default=0.0,
40
+ validators=[MinValueValidator(0.0)],
41
+ help_text="Premium tier monthly price"
42
+ )
43
+ enterprise_price = models.FloatField(
44
+ default=0.0,
45
+ validators=[MinValueValidator(0.0)],
46
+ help_text="Enterprise tier monthly price"
47
+ )
48
+
49
+ # Usage limits per tier
50
+ basic_limit = models.PositiveIntegerField(
51
+ default=1000,
52
+ help_text="Basic tier monthly usage limit"
53
+ )
54
+ premium_limit = models.PositiveIntegerField(
55
+ default=10000,
56
+ help_text="Premium tier monthly usage limit"
57
+ )
58
+ enterprise_limit = models.PositiveIntegerField(
59
+ default=0, # 0 = unlimited
60
+ help_text="Enterprise tier monthly usage limit (0 = unlimited)"
61
+ )
62
+
63
+ # Settings
64
+ is_active = models.BooleanField(
65
+ default=True,
66
+ help_text="Is this endpoint group active"
67
+ )
68
+ require_api_key = models.BooleanField(
69
+ default=True,
70
+ help_text="Require API key for access"
71
+ )
72
+
73
+ # Import and assign manager
74
+ from ..managers import EndpointGroupManager
75
+ objects = EndpointGroupManager()
76
+
77
+ class Meta:
78
+ db_table = 'endpoint_groups'
79
+ verbose_name = "Endpoint Group"
80
+ verbose_name_plural = "Endpoint Groups"
81
+ indexes = [
82
+ models.Index(fields=['name']),
83
+ models.Index(fields=['is_active']),
84
+ ]
85
+ ordering = ['name']
86
+
87
+ def __str__(self):
88
+ return self.display_name
89
+
90
+ def get_price_for_tier(self, tier: str) -> float:
91
+ """Get price for specific tier."""
92
+ tier_prices = {
93
+ 'basic': self.basic_price,
94
+ 'premium': self.premium_price,
95
+ 'enterprise': self.enterprise_price,
96
+ }
97
+ return tier_prices.get(tier, 0.0)
98
+
99
+ def get_limit_for_tier(self, tier: str) -> int:
100
+ """Get usage limit for specific tier."""
101
+ tier_limits = {
102
+ 'basic': self.basic_limit,
103
+ 'premium': self.premium_limit,
104
+ 'enterprise': self.enterprise_limit,
105
+ }
106
+ return tier_limits.get(tier, 0)
107
+
108
+
109
+ class Subscription(TimestampedModel):
110
+ """User subscriptions to endpoint groups."""
111
+
112
+ class SubscriptionStatus(models.TextChoices):
113
+ ACTIVE = "active", "Active"
114
+ INACTIVE = "inactive", "Inactive"
115
+ EXPIRED = "expired", "Expired"
116
+ CANCELLED = "cancelled", "Cancelled"
117
+ SUSPENDED = "suspended", "Suspended"
118
+
119
+ class SubscriptionTier(models.TextChoices):
120
+ BASIC = "basic", "Basic"
121
+ PREMIUM = "premium", "Premium"
122
+ ENTERPRISE = "enterprise", "Enterprise"
123
+
124
+ user = models.ForeignKey(
125
+ User,
126
+ on_delete=models.CASCADE,
127
+ related_name='subscriptions',
128
+ help_text="Subscriber"
129
+ )
130
+ endpoint_group = models.ForeignKey(
131
+ EndpointGroup,
132
+ on_delete=models.CASCADE,
133
+ related_name='subscriptions',
134
+ help_text="Endpoint group"
135
+ )
136
+
137
+ # Subscription details
138
+ tier = models.CharField(
139
+ max_length=20,
140
+ choices=SubscriptionTier.choices,
141
+ default=SubscriptionTier.BASIC,
142
+ help_text="Subscription tier"
143
+ )
144
+ status = models.CharField(
145
+ max_length=20,
146
+ choices=SubscriptionStatus.choices,
147
+ default=SubscriptionStatus.ACTIVE,
148
+ help_text="Subscription status"
149
+ )
150
+
151
+ # Pricing
152
+ monthly_price = models.FloatField(
153
+ validators=[MinValueValidator(0.0)],
154
+ help_text="Monthly subscription price"
155
+ )
156
+
157
+ # Usage tracking
158
+ usage_limit = models.PositiveIntegerField(
159
+ default=1000,
160
+ help_text="Monthly usage limit (0 = unlimited)"
161
+ )
162
+ usage_current = models.PositiveIntegerField(
163
+ default=0,
164
+ help_text="Current month usage"
165
+ )
166
+
167
+ # Billing
168
+ last_billed = models.DateTimeField(
169
+ null=True,
170
+ blank=True,
171
+ help_text="Last billing date"
172
+ )
173
+ next_billing = models.DateTimeField(
174
+ null=True,
175
+ blank=True,
176
+ help_text="Next billing date"
177
+ )
178
+
179
+ # Lifecycle
180
+ expires_at = models.DateTimeField(
181
+ null=True,
182
+ blank=True,
183
+ help_text="Subscription expiration"
184
+ )
185
+ cancelled_at = models.DateTimeField(
186
+ null=True,
187
+ blank=True,
188
+ help_text="Cancellation date"
189
+ )
190
+
191
+ # Metadata
192
+ metadata = models.JSONField(
193
+ default=dict,
194
+ help_text="Additional subscription metadata"
195
+ )
196
+
197
+ # Import and assign manager
198
+ from ..managers import SubscriptionManager
199
+ objects = SubscriptionManager()
200
+
201
+ class Meta:
202
+ db_table = 'user_subscriptions'
203
+ verbose_name = "Subscription"
204
+ verbose_name_plural = "Subscriptions"
205
+ indexes = [
206
+ models.Index(fields=['user', 'status']),
207
+ models.Index(fields=['endpoint_group', 'status']),
208
+ models.Index(fields=['status', 'expires_at']),
209
+ models.Index(fields=['next_billing']),
210
+ models.Index(fields=['created_at']),
211
+ ]
212
+ unique_together = [['user', 'endpoint_group']] # One subscription per user per group
213
+ ordering = ['-created_at']
214
+
215
+ def __str__(self):
216
+ return f"{self.user.email} - {self.endpoint_group.name} ({self.tier})"
217
+
218
+ def is_active(self) -> bool:
219
+ """Check if subscription is currently active."""
220
+ now = timezone.now()
221
+
222
+ return (
223
+ self.status == self.SubscriptionStatus.ACTIVE and
224
+ (self.expires_at is None or self.expires_at > now)
225
+ )
226
+
227
+ def is_usage_exceeded(self) -> bool:
228
+ """Check if usage limit is exceeded."""
229
+ return self.usage_limit > 0 and self.usage_current >= self.usage_limit
230
+
231
+ def get_usage_percentage(self) -> float:
232
+ """Get usage as percentage (0-100)."""
233
+ if self.usage_limit == 0:
234
+ return 0.0 # Unlimited
235
+
236
+ return min((self.usage_current / self.usage_limit) * 100, 100.0)
237
+
238
+ def can_use_api(self) -> bool:
239
+ """Check if user can use API (active and not exceeded)."""
240
+ return self.is_active() and not self.is_usage_exceeded()
241
+
242
+ def increment_usage(self, count: int = 1):
243
+ """Increment usage counter."""
244
+ self.usage_current += count
245
+ self.save(update_fields=['usage_current'])
246
+
247
+ def reset_usage(self):
248
+ """Reset usage counter (for new billing period)."""
249
+ self.usage_current = 0
250
+ self.save(update_fields=['usage_current'])
251
+
252
+ def cancel(self):
253
+ """Cancel subscription."""
254
+ self.status = self.SubscriptionStatus.CANCELLED
255
+ self.cancelled_at = timezone.now()
256
+ self.save(update_fields=['status', 'cancelled_at'])
257
+
258
+ def extend_billing_period(self):
259
+ """Extend billing period by one month."""
260
+ if self.next_billing:
261
+ self.next_billing += timedelta(days=30)
262
+ else:
263
+ self.next_billing = timezone.now() + timedelta(days=30)
264
+
265
+ if self.expires_at:
266
+ self.expires_at += timedelta(days=30)
267
+ else:
268
+ self.expires_at = timezone.now() + timedelta(days=30)
269
+
270
+ self.save(update_fields=['next_billing', 'expires_at'])
@@ -0,0 +1,102 @@
1
+ """
2
+ Tariff models for the universal payments system.
3
+ """
4
+
5
+ from django.db import models
6
+ from django.core.validators import MinValueValidator
7
+ from .base import TimestampedModel
8
+
9
+
10
+ class Tariff(TimestampedModel):
11
+ """Simple tariff plans for API access."""
12
+
13
+ name = models.CharField(
14
+ max_length=100,
15
+ unique=True,
16
+ help_text="Tariff name"
17
+ )
18
+ display_name = models.CharField(
19
+ max_length=200,
20
+ help_text="Human-readable tariff name"
21
+ )
22
+ description = models.TextField(
23
+ blank=True,
24
+ help_text="Tariff description"
25
+ )
26
+
27
+ # Pricing
28
+ monthly_price = models.FloatField(
29
+ default=0.0,
30
+ validators=[MinValueValidator(0.0)],
31
+ help_text="Monthly price in USD"
32
+ )
33
+
34
+ # Limits
35
+ request_limit = models.PositiveIntegerField(
36
+ default=1000,
37
+ help_text="Monthly request limit (0 = unlimited)"
38
+ )
39
+
40
+ # Settings
41
+ is_active = models.BooleanField(
42
+ default=True,
43
+ help_text="Is this tariff active"
44
+ )
45
+
46
+ # Import and assign manager
47
+ from ..managers import TariffManager
48
+ objects = TariffManager()
49
+
50
+ class Meta:
51
+ db_table = 'tariffs'
52
+ verbose_name = "Tariff"
53
+ verbose_name_plural = "Tariffs"
54
+ indexes = [
55
+ models.Index(fields=['is_active']),
56
+ models.Index(fields=['monthly_price']),
57
+ ]
58
+ ordering = ['monthly_price']
59
+
60
+ def __str__(self):
61
+ return f"{self.display_name} (${self.monthly_price}/month)"
62
+
63
+ @property
64
+ def is_free(self) -> bool:
65
+ """Check if this is a free tariff."""
66
+ return self.monthly_price == 0
67
+
68
+
69
+ class TariffEndpointGroup(TimestampedModel):
70
+ """Simple association between tariffs and endpoint groups."""
71
+
72
+ tariff = models.ForeignKey(
73
+ Tariff,
74
+ on_delete=models.CASCADE,
75
+ related_name='endpoint_groups',
76
+ help_text="Tariff plan"
77
+ )
78
+ from .subscriptions import EndpointGroup
79
+ endpoint_group = models.ForeignKey(
80
+ EndpointGroup,
81
+ on_delete=models.CASCADE,
82
+ related_name='tariffs',
83
+ help_text="Endpoint group"
84
+ )
85
+
86
+ is_enabled = models.BooleanField(
87
+ default=True,
88
+ help_text="Is this endpoint group enabled for this tariff"
89
+ )
90
+
91
+ # Import and assign manager
92
+ from ..managers import TariffEndpointGroupManager
93
+ objects = TariffEndpointGroupManager()
94
+
95
+ class Meta:
96
+ db_table = 'tariff_endpoint_groups'
97
+ verbose_name = "Tariff Endpoint Group"
98
+ verbose_name_plural = "Tariff Endpoint Groups"
99
+ unique_together = [['tariff', 'endpoint_group']]
100
+
101
+ def __str__(self):
102
+ return f"{self.tariff.name} - {self.endpoint_group.name}"