django-cfg 1.2.22__py3-none-any.whl → 1.2.23__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 (67) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin/__init__.py +23 -0
  3. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  4. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  5. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  6. django_cfg/apps/payments/admin/filters.py +259 -0
  7. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  8. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  9. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  10. django_cfg/apps/payments/config/__init__.py +87 -0
  11. django_cfg/apps/payments/config/module.py +162 -0
  12. django_cfg/apps/payments/config/providers.py +93 -0
  13. django_cfg/apps/payments/config/settings.py +136 -0
  14. django_cfg/apps/payments/config/utils.py +198 -0
  15. django_cfg/apps/payments/decorators.py +291 -0
  16. django_cfg/apps/payments/middleware/api_access.py +261 -0
  17. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  18. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  19. django_cfg/apps/payments/migrations/0001_initial.py +32 -11
  20. django_cfg/apps/payments/models/__init__.py +18 -0
  21. django_cfg/apps/payments/models/api_keys.py +2 -2
  22. django_cfg/apps/payments/models/balance.py +2 -2
  23. django_cfg/apps/payments/models/base.py +16 -0
  24. django_cfg/apps/payments/models/events.py +2 -2
  25. django_cfg/apps/payments/models/payments.py +2 -2
  26. django_cfg/apps/payments/models/subscriptions.py +2 -2
  27. django_cfg/apps/payments/services/__init__.py +58 -7
  28. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  29. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  30. django_cfg/apps/payments/services/cache/base.py +30 -0
  31. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  32. django_cfg/apps/payments/services/core/__init__.py +17 -0
  33. django_cfg/apps/payments/services/core/balance_service.py +449 -0
  34. django_cfg/apps/payments/services/core/payment_service.py +393 -0
  35. django_cfg/apps/payments/services/core/subscription_service.py +616 -0
  36. django_cfg/apps/payments/services/internal_types.py +266 -0
  37. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  38. django_cfg/apps/payments/services/providers/__init__.py +19 -0
  39. django_cfg/apps/payments/services/providers/base.py +137 -0
  40. django_cfg/apps/payments/services/providers/cryptapi.py +262 -0
  41. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  42. django_cfg/apps/payments/services/providers/registry.py +99 -0
  43. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  44. django_cfg/apps/payments/signals/__init__.py +13 -0
  45. django_cfg/apps/payments/signals/api_key_signals.py +150 -0
  46. django_cfg/apps/payments/signals/payment_signals.py +127 -0
  47. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  48. django_cfg/apps/payments/urls.py +5 -5
  49. django_cfg/apps/payments/utils/__init__.py +42 -0
  50. django_cfg/apps/payments/utils/config_utils.py +243 -0
  51. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  52. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  53. django_cfg/apps/support/signals.py +16 -4
  54. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  55. django_cfg/models/revolution.py +1 -1
  56. django_cfg/modules/base.py +1 -1
  57. django_cfg/modules/django_email.py +42 -4
  58. django_cfg/modules/django_unfold/dashboard.py +20 -0
  59. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/METADATA +2 -1
  60. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/RECORD +63 -26
  61. django_cfg/apps/payments/services/base.py +0 -68
  62. django_cfg/apps/payments/services/nowpayments.py +0 -78
  63. django_cfg/apps/payments/services/providers.py +0 -77
  64. django_cfg/apps/payments/services/redis_service.py +0 -215
  65. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/WHEEL +0 -0
  66. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/entry_points.txt +0 -0
  67. {django_cfg-1.2.22.dist-info → django_cfg-1.2.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,228 @@
1
+ """
2
+ Utilities for middleware processing.
3
+ """
4
+
5
+ from typing import Optional, List
6
+ from django.http import HttpRequest
7
+ from django.conf import settings
8
+
9
+
10
+ def get_client_ip(request: HttpRequest) -> Optional[str]:
11
+ """
12
+ Get client IP address from request.
13
+ Handles various proxy headers and configurations.
14
+ """
15
+
16
+ # Check for forwarded headers first
17
+ forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
18
+ if forwarded_for:
19
+ # Take first IP in chain (original client)
20
+ return forwarded_for.split(',')[0].strip()
21
+
22
+ # Check for real IP header (common with nginx)
23
+ real_ip = request.META.get('HTTP_X_REAL_IP')
24
+ if real_ip:
25
+ return real_ip.strip()
26
+
27
+ # Check for Cloudflare header
28
+ cf_ip = request.META.get('HTTP_CF_CONNECTING_IP')
29
+ if cf_ip:
30
+ return cf_ip.strip()
31
+
32
+ # Fallback to remote address
33
+ remote_addr = request.META.get('REMOTE_ADDR')
34
+ if remote_addr:
35
+ return remote_addr.strip()
36
+
37
+ return None
38
+
39
+
40
+ def is_api_request(request: HttpRequest, api_prefixes: Optional[List[str]] = None) -> bool:
41
+ """
42
+ Check if request is an API request based on path prefixes.
43
+
44
+ Args:
45
+ request: Django HTTP request
46
+ api_prefixes: List of API prefixes to check (defaults to settings)
47
+ """
48
+
49
+ if api_prefixes is None:
50
+ api_prefixes = getattr(settings, 'PAYMENTS_API_PREFIXES', ['/api/'])
51
+
52
+ path = request.path
53
+ return any(path.startswith(prefix) for prefix in api_prefixes)
54
+
55
+
56
+ def extract_api_key(request: HttpRequest) -> Optional[str]:
57
+ """
58
+ Extract API key from request headers or query parameters.
59
+ Supports multiple authentication methods.
60
+
61
+ Priority:
62
+ 1. Authorization header (Bearer token)
63
+ 2. X-API-Key header
64
+ 3. Query parameter (less secure, for testing)
65
+ """
66
+
67
+ # Method 1: Authorization header (Bearer token)
68
+ auth_header = request.META.get('HTTP_AUTHORIZATION', '')
69
+ if auth_header.startswith('Bearer '):
70
+ return auth_header[7:] # Remove 'Bearer ' prefix
71
+
72
+ # Method 2: X-API-Key header
73
+ api_key_header = request.META.get('HTTP_X_API_KEY')
74
+ if api_key_header:
75
+ return api_key_header.strip()
76
+
77
+ # Method 3: Custom header variations
78
+ custom_headers = [
79
+ 'HTTP_X_API_TOKEN',
80
+ 'HTTP_APIKEY',
81
+ 'HTTP_API_TOKEN',
82
+ ]
83
+
84
+ for header in custom_headers:
85
+ value = request.META.get(header)
86
+ if value:
87
+ return value.strip()
88
+
89
+ # Method 4: Query parameter (less secure, mainly for testing)
90
+ if getattr(settings, 'PAYMENTS_ALLOW_API_KEY_IN_QUERY', False):
91
+ query_key = request.GET.get('api_key') or request.GET.get('apikey')
92
+ if query_key:
93
+ return query_key.strip()
94
+
95
+ return None
96
+
97
+
98
+ def is_exempt_path(request: HttpRequest, exempt_paths: Optional[List[str]] = None) -> bool:
99
+ """
100
+ Check if request path is exempt from API key requirements.
101
+
102
+ Args:
103
+ request: Django HTTP request
104
+ exempt_paths: List of exempt path prefixes (defaults to settings)
105
+ """
106
+
107
+ if exempt_paths is None:
108
+ exempt_paths = getattr(settings, 'PAYMENTS_EXEMPT_PATHS', [
109
+ '/admin/',
110
+ '/cfg/',
111
+ '/api/v1/api-key/validate/',
112
+ ])
113
+
114
+ path = request.path
115
+ return any(path.startswith(exempt) for exempt in exempt_paths)
116
+
117
+
118
+ def get_request_metadata(request: HttpRequest) -> dict:
119
+ """
120
+ Extract useful metadata from request for logging and analytics.
121
+ """
122
+
123
+ return {
124
+ 'method': request.method,
125
+ 'path': request.path,
126
+ 'query_string': request.META.get('QUERY_STRING', ''),
127
+ 'user_agent': request.META.get('HTTP_USER_AGENT', ''),
128
+ 'referer': request.META.get('HTTP_REFERER', ''),
129
+ 'ip_address': get_client_ip(request),
130
+ 'content_type': request.META.get('CONTENT_TYPE', ''),
131
+ 'content_length': request.META.get('CONTENT_LENGTH', 0),
132
+ 'host': request.META.get('HTTP_HOST', ''),
133
+ 'scheme': request.scheme,
134
+ 'is_secure': request.is_secure(),
135
+ }
136
+
137
+
138
+ def should_track_request_body(request: HttpRequest, max_size: int = 10000) -> bool:
139
+ """
140
+ Determine if request body should be tracked for analytics.
141
+
142
+ Args:
143
+ request: Django HTTP request
144
+ max_size: Maximum body size to track (bytes)
145
+ """
146
+
147
+ # Check content length
148
+ content_length = request.META.get('CONTENT_LENGTH')
149
+ if content_length and int(content_length) > max_size:
150
+ return False
151
+
152
+ # Don't track file uploads
153
+ content_type = request.META.get('CONTENT_TYPE', '')
154
+ if 'multipart/form-data' in content_type:
155
+ return False
156
+
157
+ # Don't track binary content
158
+ if 'application/octet-stream' in content_type:
159
+ return False
160
+
161
+ # Don't track sensitive endpoints
162
+ sensitive_paths = getattr(settings, 'PAYMENTS_SENSITIVE_PATHS', [
163
+ '/api/v1/api-key/',
164
+ '/api/v1/payment/',
165
+ '/api/v1/subscription/',
166
+ ])
167
+
168
+ path = request.path
169
+ if any(path.startswith(sensitive) for sensitive in sensitive_paths):
170
+ return False
171
+
172
+ return True
173
+
174
+
175
+ def should_track_response_body(response, max_size: int = 10000) -> bool:
176
+ """
177
+ Determine if response body should be tracked for analytics.
178
+
179
+ Args:
180
+ response: Django HTTP response
181
+ max_size: Maximum body size to track (bytes)
182
+ """
183
+
184
+ # Don't track large responses
185
+ if hasattr(response, 'content') and len(response.content) > max_size:
186
+ return False
187
+
188
+ # Only track successful JSON responses
189
+ if not (200 <= response.status_code < 300):
190
+ return False
191
+
192
+ # Check content type
193
+ content_type = response.get('Content-Type', '')
194
+ if 'application/json' not in content_type:
195
+ return False
196
+
197
+ return True
198
+
199
+
200
+ def format_error_response(error_code: str,
201
+ message: str,
202
+ status_code: int = 400,
203
+ additional_data: Optional[dict] = None) -> dict:
204
+ """
205
+ Format standardized error response for middleware.
206
+
207
+ Args:
208
+ error_code: Machine-readable error code
209
+ message: Human-readable error message
210
+ status_code: HTTP status code
211
+ additional_data: Additional error data
212
+ """
213
+
214
+ from django.utils import timezone
215
+
216
+ error_response = {
217
+ 'error': {
218
+ 'code': error_code,
219
+ 'message': message,
220
+ 'status_code': status_code,
221
+ 'timestamp': timezone.now().isoformat(),
222
+ }
223
+ }
224
+
225
+ if additional_data:
226
+ error_response['error'].update(additional_data)
227
+
228
+ return error_response
@@ -0,0 +1,94 @@
1
+ """
2
+ Validation utilities for payments module.
3
+
4
+ Basic validation functions for API keys and subscription access.
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional, Dict, Any
9
+ from django.contrib.auth import get_user_model
10
+ from django.utils import timezone
11
+
12
+ from ..models import APIKey, Subscription
13
+
14
+ User = get_user_model()
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def validate_api_key(api_key: str) -> bool:
19
+ """
20
+ Validate API key.
21
+
22
+ Args:
23
+ api_key: API key to validate
24
+
25
+ Returns:
26
+ True if valid, False otherwise
27
+ """
28
+ try:
29
+ key = APIKey.objects.select_related('user').get(
30
+ key_value=api_key,
31
+ is_active=True
32
+ )
33
+
34
+ # Check if key is expired
35
+ if key.expires_at and key.expires_at < timezone.now():
36
+ return False
37
+
38
+ return True
39
+
40
+ except APIKey.DoesNotExist:
41
+ return False
42
+ except Exception as e:
43
+ logger.error(f"Error validating API key: {e}")
44
+ return False
45
+
46
+
47
+ def check_subscription_access(user_id: int, endpoint_group: str) -> Dict[str, Any]:
48
+ """
49
+ Check subscription access for user and endpoint group.
50
+
51
+ Args:
52
+ user_id: User ID
53
+ endpoint_group: Endpoint group name
54
+
55
+ Returns:
56
+ Access check result dictionary
57
+ """
58
+ try:
59
+ subscription = Subscription.objects.select_related('endpoint_group').get(
60
+ user_id=user_id,
61
+ endpoint_group__name=endpoint_group,
62
+ status='active',
63
+ expires_at__gt=timezone.now()
64
+ )
65
+
66
+ # Check usage limits
67
+ usage_percentage = (subscription.current_usage / subscription.monthly_limit) * 100
68
+ remaining_requests = subscription.monthly_limit - subscription.current_usage
69
+
70
+ return {
71
+ 'allowed': remaining_requests > 0,
72
+ 'subscription_id': str(subscription.id),
73
+ 'remaining_requests': remaining_requests,
74
+ 'usage_percentage': usage_percentage,
75
+ 'reason': 'Active subscription' if remaining_requests > 0 else 'Usage limit exceeded'
76
+ }
77
+
78
+ except Subscription.DoesNotExist:
79
+ return {
80
+ 'allowed': False,
81
+ 'reason': 'No active subscription found',
82
+ 'subscription_id': None,
83
+ 'remaining_requests': 0,
84
+ 'usage_percentage': 0
85
+ }
86
+ except Exception as e:
87
+ logger.error(f"Error checking subscription access: {e}")
88
+ return {
89
+ 'allowed': False,
90
+ 'reason': f'Access check failed: {str(e)}',
91
+ 'subscription_id': None,
92
+ 'remaining_requests': 0,
93
+ 'usage_percentage': 0
94
+ }
@@ -2,6 +2,8 @@ from django.db.models.signals import post_save
2
2
  from django.dispatch import receiver
3
3
  import traceback
4
4
  import logging
5
+ import socket
6
+ from smtplib import SMTPException
5
7
 
6
8
  from .models import Message, Ticket
7
9
  from .utils.support_email_service import SupportEmailService
@@ -10,7 +12,7 @@ from django_cfg.modules.django_telegram import DjangoTelegram
10
12
  logger = logging.getLogger(__name__)
11
13
 
12
14
  @receiver(post_save, sender=Message)
13
- def notify_on_message(sender, instance, created, **kwargs):
15
+ def notify_on_message(sender, instance: Message, created: bool, **kwargs):
14
16
  """Send notifications when a new message is created."""
15
17
  logger.info(f"🔔 Signal triggered: Message {instance.uuid} created={created}")
16
18
 
@@ -32,9 +34,14 @@ def notify_on_message(sender, instance, created, **kwargs):
32
34
  email_service = SupportEmailService(user)
33
35
  email_service.send_support_reply_email(instance)
34
36
  logger.info(f" 📬 Email sent successfully!")
37
+ except (socket.timeout, TimeoutError, SMTPException) as e:
38
+ logger.warning(f" ⚠️ Email service timeout/error: {e}")
39
+ logger.info(f" 📝 Message processed successfully, email notification failed")
40
+ # Do not re-raise to prevent blocking the main process
35
41
  except Exception as e:
36
42
  logger.error(f" ❌ Failed to send email notification: {e}")
37
- traceback.print_exc()
43
+ logger.debug(f" 🔍 Exception details: {traceback.format_exc()}")
44
+ # Do not re-raise to prevent blocking the main process
38
45
  else:
39
46
  logger.info(f" ⏭️ Not sending email (staff: {instance.sender.is_staff}, from_author: {instance.is_from_author})")
40
47
 
@@ -60,7 +67,7 @@ def notify_on_message(sender, instance, created, **kwargs):
60
67
 
61
68
 
62
69
  @receiver(post_save, sender=Ticket)
63
- def notify_on_ticket_created(sender, instance, created, **kwargs):
70
+ def notify_on_ticket_created(sender, instance: Ticket, created: bool, **kwargs):
64
71
  """Send notification when a new ticket is created."""
65
72
  if not created:
66
73
  return
@@ -68,5 +75,10 @@ def notify_on_ticket_created(sender, instance, created, **kwargs):
68
75
  try:
69
76
  email_service = SupportEmailService(instance.user)
70
77
  email_service.send_ticket_created_email(instance)
78
+ logger.info(f" 📬 Ticket creation email sent successfully!")
79
+ except (socket.timeout, TimeoutError, SMTPException) as e:
80
+ logger.warning(f" ⚠️ Email service timeout/error for ticket creation: {e}")
81
+ logger.info(f" 📝 Ticket created successfully, email notification failed")
71
82
  except Exception as e:
72
- logger.error(f"Failed to send ticket creation email: {e}")
83
+ logger.error(f"Failed to send ticket creation email: {e}")
84
+ logger.debug(f" 🔍 Exception details: {traceback.format_exc()}")
@@ -197,7 +197,7 @@
197
197
  submitBtn.disabled = true;
198
198
 
199
199
  try {
200
- const response = await fetch(`{% url 'send-message-ajax' ticket_uuid=ticket.uuid %}`, {
200
+ const response = await fetch(`{% url 'cfg_support:send-message-ajax' ticket_uuid=ticket.uuid %}`, {
201
201
  method: 'POST',
202
202
  headers: {
203
203
  'Content-Type': 'application/json',
@@ -93,7 +93,7 @@ class ExtendedRevolutionConfig(BaseDjangoRevolutionConfig):
93
93
  knowbase_enabled = base_module.is_knowbase_enabled()
94
94
  agents_enabled = base_module.is_agents_enabled()
95
95
  tasks_enabled = base_module.should_enable_tasks()
96
- payments_enabled = base_module.enable_payments()
96
+ payments_enabled = base_module.is_payments_enabled()
97
97
 
98
98
  # Add Support zone if enabled
99
99
  default_support_zone = 'cfg_support'
@@ -175,7 +175,7 @@ class BaseCfgModule(ABC):
175
175
  """
176
176
  return self._get_config_key('enable_maintenance', False)
177
177
 
178
- def enable_payments(self) -> bool:
178
+ def is_payments_enabled(self) -> bool:
179
179
  """
180
180
  Check if django-cfg Payments is enabled.
181
181
 
@@ -6,6 +6,9 @@ without requiring manual parameter passing.
6
6
  """
7
7
 
8
8
  from typing import List, Optional, Dict, Any, Union
9
+ import socket
10
+ from smtplib import SMTPException
11
+ import logging
9
12
  from django.core.mail import send_mail, EmailMultiAlternatives
10
13
  from django.template.loader import render_to_string
11
14
  from django.utils.html import strip_tags
@@ -13,6 +16,8 @@ from django.conf import settings
13
16
 
14
17
  from . import BaseCfgModule
15
18
 
19
+ logger = logging.getLogger(__name__)
20
+
16
21
 
17
22
  class DjangoEmailService(BaseCfgModule):
18
23
  """
@@ -30,6 +35,35 @@ class DjangoEmailService(BaseCfgModule):
30
35
  self.config = self.get_config()
31
36
  self.email_config = getattr(self.config, 'email', None)
32
37
 
38
+ def _handle_email_sending(self, email_func, *args, **kwargs):
39
+ """
40
+ Wrapper for email sending with proper timeout/exception handling.
41
+
42
+ Args:
43
+ email_func: Email sending function to call
44
+ *args, **kwargs: Arguments to pass to the function
45
+
46
+ Returns:
47
+ Result of email_func or 0 on timeout/error
48
+ """
49
+ try:
50
+ result = email_func(*args, **kwargs)
51
+ logger.debug(f"Email sent successfully: {email_func.__name__}")
52
+ return result
53
+ except (socket.timeout, TimeoutError) as e:
54
+ logger.warning(f"Email sending timeout: {e}")
55
+ logger.info("Consider checking SMTP server configuration or network connectivity")
56
+ return 0
57
+ except SMTPException as e:
58
+ logger.warning(f"SMTP error during email sending: {e}")
59
+ logger.info("Email service temporarily unavailable")
60
+ return 0
61
+ except Exception as e:
62
+ logger.error(f"Unexpected error during email sending: {e}")
63
+ if not kwargs.get('fail_silently', False):
64
+ raise
65
+ return 0
66
+
33
67
  def send_simple(
34
68
  self,
35
69
  subject: str,
@@ -53,7 +87,8 @@ class DjangoEmailService(BaseCfgModule):
53
87
  """
54
88
  from_email = self._get_formatted_from_email(from_email)
55
89
 
56
- return send_mail(
90
+ return self._handle_email_sending(
91
+ send_mail,
57
92
  subject=subject,
58
93
  message=message,
59
94
  from_email=from_email,
@@ -89,7 +124,8 @@ class DjangoEmailService(BaseCfgModule):
89
124
  if text_message is None:
90
125
  text_message = strip_tags(html_message)
91
126
 
92
- return send_mail(
127
+ return self._handle_email_sending(
128
+ send_mail,
93
129
  subject=subject,
94
130
  message=text_message,
95
131
  from_email=from_email,
@@ -174,7 +210,7 @@ class DjangoEmailService(BaseCfgModule):
174
210
  if not html_content and not text_content:
175
211
  raise ValueError("Either html_content or text_content must be provided")
176
212
 
177
- try:
213
+ def _send_multipart_email():
178
214
  email = EmailMultiAlternatives(
179
215
  subject=subject,
180
216
  body=text_content or strip_tags(html_content or ''),
@@ -191,7 +227,9 @@ class DjangoEmailService(BaseCfgModule):
191
227
 
192
228
  email.send(fail_silently=fail_silently)
193
229
  return True
194
-
230
+
231
+ try:
232
+ return self._handle_email_sending(_send_multipart_email) or False
195
233
  except Exception as e:
196
234
  if not fail_silently:
197
235
  raise e
@@ -163,8 +163,28 @@ class DashboardManager(BaseCfgModule):
163
163
  ]
164
164
  ))
165
165
 
166
+ # Add Payments section if enabled
167
+ if self.is_payments_enabled():
168
+ navigation_sections.append(NavigationSection(
169
+ title="Payments",
170
+ separator=True,
171
+ collapsible=True,
172
+ items=[
173
+ NavigationItem(title="Payments", icon=Icons.ACCOUNT_BALANCE, link="/admin/django_cfg_payments/universalpayment/"),
174
+ NavigationItem(title="Subscriptions", icon=Icons.PERSON_ADD, link="/admin/django_cfg_payments/subscription/"),
175
+ NavigationItem(title="API Keys", icon=Icons.KEY, link="/admin/django_cfg_payments/apikey/"),
176
+ NavigationItem(title="Balances", icon=Icons.ACCOUNT_BALANCE_WALLET, link="/admin/django_cfg_payments/userbalance/"),
177
+ NavigationItem(title="Transactions", icon=Icons.DESCRIPTION, link="/admin/django_cfg_payments/transaction/"),
178
+ NavigationItem(title="Currencies", icon=Icons.ACCOUNT_CIRCLE, link="/admin/django_cfg_payments/currency/"),
179
+ NavigationItem(title="Currency Networks", icon=Icons.LINK, link="/admin/django_cfg_payments/currencynetwork/"),
180
+ NavigationItem(title="Endpoint Groups", icon=Icons.GROUP, link="/admin/django_cfg_payments/endpointgroup/"),
181
+ NavigationItem(title="Tariffs", icon=Icons.SETTINGS, link="/admin/django_cfg_payments/tariff/"),
182
+ ]
183
+ ))
184
+
166
185
  # Convert all NavigationSection objects to dictionaries
167
186
  return [section.to_dict() for section in navigation_sections]
187
+
168
188
 
169
189
 
170
190
  def get_unfold_config(self) -> Dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.2.22
3
+ Version: 1.2.23
4
4
  Summary: 🚀 Next-gen Django configuration: type-safety, AI features, blazing-fast setup, and automated best practices — all in one.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://docs.djangocfg.com
@@ -146,6 +146,7 @@ Description-Content-Type: text/markdown
146
146
 
147
147
  # 🚀 Django-CFG: Enterprise Django Configuration Framework
148
148
 
149
+
149
150
  [![Python Version](https://img.shields.io/pypi/pyversions/django-cfg.svg?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/django-cfg/)
150
151
  [![Django Version](https://img.shields.io/pypi/djversions/django-cfg.svg?style=flat-square&logo=django&logoColor=white)](https://pypi.org/project/django-cfg/)
151
152
  [![PyPI Version](https://img.shields.io/pypi/v/django-cfg.svg?style=flat-square&logo=pypi&logoColor=white)](https://pypi.org/project/django-cfg/)