django-cfg 1.3.7__py3-none-any.whl β†’ 1.3.11__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 (246) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +269 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +183 -460
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +153 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_card.html +121 -0
  48. django_cfg/apps/payments/admin_interface/templates/payments/components/payment_qr_code.html +95 -0
  49. django_cfg/apps/payments/admin_interface/templates/payments/components/progress_bar.html +37 -0
  50. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_stats.html +60 -0
  51. django_cfg/apps/payments/admin_interface/templates/payments/components/status_badge.html +41 -0
  52. django_cfg/apps/payments/admin_interface/templates/payments/components/status_overview.html +83 -0
  53. django_cfg/apps/payments/admin_interface/templates/payments/payment_detail.html +363 -0
  54. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +43 -17
  55. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  56. django_cfg/apps/payments/admin_interface/views/api/payments.py +102 -0
  57. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +109 -63
  58. django_cfg/apps/payments/admin_interface/views/forms.py +5 -1
  59. django_cfg/apps/payments/config/__init__.py +14 -15
  60. django_cfg/apps/payments/config/django_cfg_integration.py +59 -1
  61. django_cfg/apps/payments/config/helpers.py +8 -13
  62. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  63. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  64. django_cfg/apps/payments/middleware/api_access.py +32 -6
  65. django_cfg/apps/payments/migrations/0001_initial.py +33 -46
  66. django_cfg/apps/payments/migrations/0002_rename_payments_un_user_id_7f6e79_idx_payments_un_user_id_8ce187_idx_and_more.py +46 -0
  67. django_cfg/apps/payments/migrations/0003_universalpayment_status_changed_at.py +25 -0
  68. django_cfg/apps/payments/models/balance.py +12 -0
  69. django_cfg/apps/payments/models/currencies.py +106 -32
  70. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  71. django_cfg/apps/payments/models/managers/payment_managers.py +142 -25
  72. django_cfg/apps/payments/models/payments.py +94 -0
  73. django_cfg/apps/payments/services/core/base.py +4 -4
  74. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  75. django_cfg/apps/payments/services/core/payment_service.py +266 -39
  76. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  77. django_cfg/apps/payments/services/providers/base.py +303 -41
  78. django_cfg/apps/payments/services/providers/models/__init__.py +42 -0
  79. django_cfg/apps/payments/services/providers/models/base.py +145 -0
  80. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  81. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  82. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  83. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  84. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  85. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  86. django_cfg/apps/payments/services/providers/nowpayments/provider.py +557 -0
  87. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  88. django_cfg/apps/payments/services/providers/registry.py +9 -37
  89. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  90. django_cfg/apps/payments/services/types/requests.py +19 -7
  91. django_cfg/apps/payments/signals/payment_signals.py +31 -2
  92. django_cfg/apps/payments/static/payments/js/api-client.js +29 -6
  93. django_cfg/apps/payments/static/payments/js/payment-detail.js +167 -0
  94. django_cfg/apps/payments/static/payments/js/payment-form.js +98 -32
  95. django_cfg/apps/payments/tasks/__init__.py +39 -0
  96. django_cfg/apps/payments/tasks/types.py +73 -0
  97. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  98. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  99. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  100. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  101. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  102. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  103. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  104. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  105. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  106. django_cfg/apps/payments/templatetags/payment_tags.py +8 -0
  107. django_cfg/apps/payments/urls.py +3 -2
  108. django_cfg/apps/payments/urls_admin.py +1 -1
  109. django_cfg/apps/payments/views/api/currencies.py +8 -5
  110. django_cfg/apps/payments/views/overview/services.py +2 -2
  111. django_cfg/apps/payments/views/serializers/currencies.py +22 -8
  112. django_cfg/apps/support/admin/__init__.py +10 -1
  113. django_cfg/apps/support/admin/support_admin.py +338 -141
  114. django_cfg/apps/tasks/admin/__init__.py +11 -0
  115. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  116. django_cfg/apps/tasks/static/tasks/css/dashboard.css +68 -217
  117. django_cfg/apps/tasks/static/tasks/js/api.js +40 -84
  118. django_cfg/apps/tasks/static/tasks/js/components/DataManager.js +24 -0
  119. django_cfg/apps/tasks/static/tasks/js/components/TabManager.js +85 -0
  120. django_cfg/apps/tasks/static/tasks/js/components/TaskRenderer.js +216 -0
  121. django_cfg/apps/tasks/static/tasks/js/dashboard/main.mjs +245 -0
  122. django_cfg/apps/tasks/static/tasks/js/dashboard/overview.mjs +123 -0
  123. django_cfg/apps/tasks/static/tasks/js/dashboard/queues.mjs +120 -0
  124. django_cfg/apps/tasks/static/tasks/js/dashboard/tasks.mjs +350 -0
  125. django_cfg/apps/tasks/static/tasks/js/dashboard/workers.mjs +169 -0
  126. django_cfg/apps/tasks/tasks/__init__.py +10 -0
  127. django_cfg/apps/tasks/tasks/demo_tasks.py +133 -0
  128. django_cfg/apps/tasks/templates/tasks/components/management_actions.html +42 -45
  129. django_cfg/apps/tasks/templates/tasks/components/{status_cards.html β†’ overview_content.html} +30 -11
  130. django_cfg/apps/tasks/templates/tasks/components/queues_content.html +19 -0
  131. django_cfg/apps/tasks/templates/tasks/components/tab_navigation.html +16 -10
  132. django_cfg/apps/tasks/templates/tasks/components/tasks_content.html +51 -0
  133. django_cfg/apps/tasks/templates/tasks/components/workers_content.html +30 -0
  134. django_cfg/apps/tasks/templates/tasks/layout/base.html +117 -0
  135. django_cfg/apps/tasks/templates/tasks/pages/dashboard.html +82 -0
  136. django_cfg/apps/tasks/templates/tasks/partials/task_row_template.html +40 -0
  137. django_cfg/apps/tasks/templates/tasks/widgets/task_filters.html +37 -0
  138. django_cfg/apps/tasks/templates/tasks/widgets/task_footer.html +41 -0
  139. django_cfg/apps/tasks/templates/tasks/widgets/task_table.html +50 -0
  140. django_cfg/apps/tasks/urls.py +2 -2
  141. django_cfg/apps/tasks/urls_admin.py +2 -2
  142. django_cfg/apps/tasks/utils/__init__.py +1 -0
  143. django_cfg/apps/tasks/utils/simulator.py +356 -0
  144. django_cfg/apps/tasks/views/__init__.py +16 -0
  145. django_cfg/apps/tasks/views/api.py +569 -0
  146. django_cfg/apps/tasks/views/dashboard.py +58 -0
  147. django_cfg/config.py +1 -1
  148. django_cfg/core/config.py +10 -5
  149. django_cfg/core/generation.py +1 -1
  150. django_cfg/core/integration/__init__.py +21 -0
  151. django_cfg/management/commands/__init__.py +13 -1
  152. django_cfg/management/commands/migrate_all.py +9 -3
  153. django_cfg/management/commands/migrator.py +11 -6
  154. django_cfg/management/commands/rundramatiq.py +3 -2
  155. django_cfg/management/commands/rundramatiq_simulator.py +430 -0
  156. django_cfg/middleware/__init__.py +0 -2
  157. django_cfg/models/api_keys.py +115 -0
  158. django_cfg/models/constance.py +0 -11
  159. django_cfg/models/payments.py +137 -3
  160. django_cfg/modules/django_admin/__init__.py +64 -0
  161. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  162. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  163. django_cfg/modules/django_admin/decorators/display.py +106 -0
  164. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  165. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  166. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  167. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  168. django_cfg/modules/django_admin/models/__init__.py +20 -0
  169. django_cfg/modules/django_admin/models/action_models.py +33 -0
  170. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  171. django_cfg/modules/django_admin/models/base.py +26 -0
  172. django_cfg/modules/django_admin/models/display_models.py +31 -0
  173. django_cfg/modules/django_admin/utils/badges.py +159 -0
  174. django_cfg/modules/django_admin/utils/displays.py +247 -0
  175. django_cfg/modules/django_currency/__init__.py +2 -2
  176. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  177. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  178. django_cfg/modules/django_currency/core/converter.py +12 -12
  179. django_cfg/modules/django_currency/database/__init__.py +2 -2
  180. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  181. django_cfg/modules/django_llm/llm/client.py +10 -2
  182. django_cfg/modules/django_tasks.py +54 -21
  183. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  184. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  185. django_cfg/modules/django_unfold/dashboard.py +14 -13
  186. django_cfg/modules/django_unfold/models/config.py +1 -1
  187. django_cfg/registry/core.py +7 -9
  188. django_cfg/registry/third_party.py +2 -2
  189. django_cfg/template_archive/django_sample.zip +0 -0
  190. {django_cfg-1.3.7.dist-info β†’ django_cfg-1.3.11.dist-info}/METADATA +2 -1
  191. {django_cfg-1.3.7.dist-info β†’ django_cfg-1.3.11.dist-info}/RECORD +198 -160
  192. django_cfg/apps/accounts/admin/activity.py +0 -96
  193. django_cfg/apps/accounts/admin/group.py +0 -17
  194. django_cfg/apps/accounts/admin/otp.py +0 -59
  195. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  196. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  197. django_cfg/apps/accounts/admin/user.py +0 -300
  198. django_cfg/apps/agents/core/agent.py +0 -281
  199. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  200. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  201. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  202. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  203. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  204. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  205. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  206. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  207. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  208. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  209. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  210. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  211. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  212. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  213. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  214. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  215. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  216. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  217. django_cfg/apps/payments/config/constance/__init__.py +0 -22
  218. django_cfg/apps/payments/config/constance/config_service.py +0 -123
  219. django_cfg/apps/payments/config/constance/fields.py +0 -69
  220. django_cfg/apps/payments/config/constance/settings.py +0 -160
  221. django_cfg/apps/payments/services/providers/nowpayments.py +0 -478
  222. django_cfg/apps/tasks/admin.py +0 -320
  223. django_cfg/apps/tasks/static/tasks/js/dashboard.js +0 -614
  224. django_cfg/apps/tasks/static/tasks/js/modals.js +0 -452
  225. django_cfg/apps/tasks/static/tasks/js/notifications.js +0 -144
  226. django_cfg/apps/tasks/static/tasks/js/task-monitor.js +0 -454
  227. django_cfg/apps/tasks/static/tasks/js/theme.js +0 -77
  228. django_cfg/apps/tasks/templates/tasks/base.html +0 -96
  229. django_cfg/apps/tasks/templates/tasks/components/info_cards.html +0 -85
  230. django_cfg/apps/tasks/templates/tasks/components/overview_tab.html +0 -22
  231. django_cfg/apps/tasks/templates/tasks/components/queues_tab.html +0 -19
  232. django_cfg/apps/tasks/templates/tasks/components/task_details_modal.html +0 -103
  233. django_cfg/apps/tasks/templates/tasks/components/tasks_tab.html +0 -32
  234. django_cfg/apps/tasks/templates/tasks/components/workers_tab.html +0 -29
  235. django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -29
  236. django_cfg/apps/tasks/views.py +0 -461
  237. django_cfg/management/commands/auto_generate.py +0 -486
  238. django_cfg/middleware/static_nocache.py +0 -55
  239. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  240. /django_cfg/modules/{django_unfold β†’ django_admin}/icons/README.md +0 -0
  241. /django_cfg/modules/{django_unfold β†’ django_admin}/icons/__init__.py +0 -0
  242. /django_cfg/modules/{django_unfold β†’ django_admin}/icons/constants.py +0 -0
  243. /django_cfg/modules/{django_unfold β†’ django_admin}/icons/generate_icons.py +0 -0
  244. {django_cfg-1.3.7.dist-info β†’ django_cfg-1.3.11.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.3.7.dist-info β†’ django_cfg-1.3.11.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.3.7.dist-info β†’ django_cfg-1.3.11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,308 @@
1
+ """
2
+ Background tasks for API usage tracking and statistics.
3
+ """
4
+ import dramatiq
5
+ import logging
6
+ import time
7
+ from typing import Dict, Any, List
8
+ from django.db import transaction
9
+ from django.db.models import F
10
+ from django.utils import timezone
11
+ from django.core.cache import cache
12
+
13
+ from ..models import APIKey, Subscription
14
+ from .types import (
15
+ TaskResult,
16
+ UsageUpdateRequest,
17
+ UsageUpdateResult,
18
+ BatchUpdateResult,
19
+ CleanupResult
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ @dramatiq.actor(queue_name="payments")
25
+ def update_api_key_usage_async(
26
+ api_key_id: str,
27
+ ip_address: str = None,
28
+ increment: int = 1
29
+ ) -> UsageUpdateResult:
30
+ """
31
+ Update API key usage counters asynchronously.
32
+
33
+ Args:
34
+ api_key_id: API key UUID to update
35
+ ip_address: Client IP address for logging
36
+ increment: Number to increment (default: 1)
37
+
38
+ Returns:
39
+ Update result with statistics
40
+ """
41
+ start_time = time.time()
42
+
43
+ try:
44
+ with transaction.atomic():
45
+ # Use F() expressions for atomic updates
46
+ updated_count = APIKey.objects.filter(id=api_key_id).update(
47
+ total_requests=F('total_requests') + increment,
48
+ last_used_at=timezone.now(),
49
+ updated_at=timezone.now()
50
+ )
51
+
52
+ if updated_count == 0:
53
+ logger.warning(f"API key not found: {api_key_id}")
54
+ return UsageUpdateResult(
55
+ status='error',
56
+ error='API key not found',
57
+ resource_id=api_key_id,
58
+ increment=increment
59
+ )
60
+
61
+ # Get updated values for logging
62
+ api_key = APIKey.objects.get(id=api_key_id)
63
+
64
+ processing_time = (time.time() - start_time) * 1000
65
+
66
+ logger.debug(f"API key usage updated", extra={
67
+ 'api_key_id': api_key_id,
68
+ 'user_id': api_key.user.id,
69
+ 'total_requests': api_key.total_requests,
70
+ 'increment': increment,
71
+ 'ip_address': ip_address,
72
+ 'processing_time_ms': round(processing_time, 2)
73
+ })
74
+
75
+ return UsageUpdateResult(
76
+ status='success',
77
+ resource_id=api_key_id,
78
+ total_requests=api_key.total_requests,
79
+ increment=increment,
80
+ user_id=api_key.user.id,
81
+ processing_time_ms=round(processing_time, 2)
82
+ )
83
+
84
+ except Exception as e:
85
+ logger.error(f"Failed to update API key usage", extra={
86
+ 'api_key_id': api_key_id,
87
+ 'error': str(e),
88
+ 'ip_address': ip_address
89
+ })
90
+ raise # Re-raise for Dramatiq retry logic
91
+
92
+ @dramatiq.actor(queue_name="payments")
93
+ def update_subscription_usage_async(
94
+ subscription_id: str,
95
+ increment: int = 1
96
+ ) -> UsageUpdateResult:
97
+ """
98
+ Update subscription usage counters asynchronously.
99
+
100
+ Args:
101
+ subscription_id: Subscription UUID to update
102
+ increment: Number to increment (default: 1)
103
+
104
+ Returns:
105
+ Update result with statistics
106
+ """
107
+ start_time = time.time()
108
+
109
+ try:
110
+ with transaction.atomic():
111
+ # Use F() expressions for atomic updates
112
+ updated_count = Subscription.objects.filter(id=subscription_id).update(
113
+ total_requests=F('total_requests') + increment,
114
+ last_request_at=timezone.now(),
115
+ updated_at=timezone.now()
116
+ )
117
+
118
+ if updated_count == 0:
119
+ logger.warning(f"Subscription not found: {subscription_id}")
120
+ return UsageUpdateResult(
121
+ status='error',
122
+ error='Subscription not found',
123
+ resource_id=subscription_id,
124
+ increment=increment
125
+ )
126
+
127
+ # Get updated values for logging
128
+ subscription = Subscription.objects.get(id=subscription_id)
129
+
130
+ processing_time = (time.time() - start_time) * 1000
131
+
132
+ logger.debug(f"Subscription usage updated", extra={
133
+ 'subscription_id': subscription_id,
134
+ 'user_id': subscription.user.id,
135
+ 'total_requests': subscription.total_requests,
136
+ 'increment': increment,
137
+ 'processing_time_ms': round(processing_time, 2)
138
+ })
139
+
140
+ return UsageUpdateResult(
141
+ status='success',
142
+ resource_id=subscription_id,
143
+ total_requests=subscription.total_requests,
144
+ increment=increment,
145
+ user_id=subscription.user.id,
146
+ processing_time_ms=round(processing_time, 2)
147
+ )
148
+
149
+ except Exception as e:
150
+ logger.error(f"Failed to update subscription usage", extra={
151
+ 'subscription_id': subscription_id,
152
+ 'error': str(e)
153
+ })
154
+ raise # Re-raise for Dramatiq retry logic
155
+
156
+ @dramatiq.actor(queue_name="payments")
157
+ def batch_update_usage_counters() -> BatchUpdateResult:
158
+ """
159
+ Batch update usage counters from cache to reduce database load.
160
+
161
+ This task processes accumulated usage data from Redis cache
162
+ and performs batch updates to the database.
163
+
164
+ Returns:
165
+ Batch processing results
166
+ """
167
+ start_time = time.time()
168
+ api_keys_updated = 0
169
+ subscriptions_updated = 0
170
+ errors = []
171
+
172
+ try:
173
+ # Process API key usage counters
174
+ api_key_pattern = "api_usage_pending:*"
175
+ api_key_keys = cache.keys(api_key_pattern)
176
+
177
+ for cache_key in api_key_keys:
178
+ try:
179
+ # Extract API key ID from cache key
180
+ api_key_id = cache_key.split(':')[-1]
181
+ pending_count = cache.get(cache_key, 0)
182
+
183
+ if pending_count > 0:
184
+ # Update in background
185
+ update_api_key_usage_async.send(
186
+ api_key_id=api_key_id,
187
+ increment=pending_count
188
+ )
189
+
190
+ # Clear cache
191
+ cache.delete(cache_key)
192
+ api_keys_updated += 1
193
+
194
+ except Exception as e:
195
+ errors.append({
196
+ 'type': 'api_key',
197
+ 'cache_key': cache_key,
198
+ 'error': str(e)
199
+ })
200
+
201
+ # Process subscription usage counters
202
+ subscription_pattern = "subscription_usage_pending:*"
203
+ subscription_keys = cache.keys(subscription_pattern)
204
+
205
+ for cache_key in subscription_keys:
206
+ try:
207
+ # Extract subscription ID from cache key
208
+ subscription_id = cache_key.split(':')[-1]
209
+ pending_count = cache.get(cache_key, 0)
210
+
211
+ if pending_count > 0:
212
+ # Update in background
213
+ update_subscription_usage_async.send(
214
+ subscription_id=subscription_id,
215
+ increment=pending_count
216
+ )
217
+
218
+ # Clear cache
219
+ cache.delete(cache_key)
220
+ subscriptions_updated += 1
221
+
222
+ except Exception as e:
223
+ errors.append({
224
+ 'type': 'subscription',
225
+ 'cache_key': cache_key,
226
+ 'error': str(e)
227
+ })
228
+
229
+ processing_time = (time.time() - start_time) * 1000
230
+
231
+ logger.info(f"Batch usage update completed", extra={
232
+ 'api_keys_updated': api_keys_updated,
233
+ 'subscriptions_updated': subscriptions_updated,
234
+ 'errors_count': len(errors),
235
+ 'processing_time_ms': round(processing_time, 2)
236
+ })
237
+
238
+ return BatchUpdateResult(
239
+ status='success',
240
+ api_keys_updated=api_keys_updated,
241
+ subscriptions_updated=subscriptions_updated,
242
+ errors=errors,
243
+ total_items=api_keys_updated + subscriptions_updated,
244
+ processing_time_ms=round(processing_time, 2)
245
+ )
246
+
247
+ except Exception as e:
248
+ logger.error(f"Batch usage update failed: {e}")
249
+ errors.append({
250
+ 'type': 'batch_processing',
251
+ 'error': str(e)
252
+ })
253
+ return BatchUpdateResult(
254
+ status='error',
255
+ api_keys_updated=api_keys_updated,
256
+ subscriptions_updated=subscriptions_updated,
257
+ errors=errors,
258
+ total_items=api_keys_updated + subscriptions_updated,
259
+ error=str(e)
260
+ )
261
+
262
+ @dramatiq.actor(queue_name="payments")
263
+ def cleanup_stale_usage_cache() -> CleanupResult:
264
+ """
265
+ Cleanup stale usage tracking cache entries.
266
+
267
+ Removes old cache entries that might have been left behind
268
+ due to processing errors or system restarts.
269
+
270
+ Returns:
271
+ Cleanup results
272
+ """
273
+ try:
274
+ cleanup_count = 0
275
+
276
+ # Cleanup old API key usage cache
277
+ api_key_keys = cache.keys("api_usage_pending:*")
278
+ for key in api_key_keys:
279
+ # Check if cache entry is older than 1 hour
280
+ ttl = cache.ttl(key)
281
+ if ttl is not None and ttl < 3600: # Less than 1 hour remaining
282
+ cache.delete(key)
283
+ cleanup_count += 1
284
+
285
+ # Cleanup old subscription usage cache
286
+ subscription_keys = cache.keys("subscription_usage_pending:*")
287
+ for key in subscription_keys:
288
+ ttl = cache.ttl(key)
289
+ if ttl is not None and ttl < 3600:
290
+ cache.delete(key)
291
+ cleanup_count += 1
292
+
293
+ logger.info(f"Cleaned up {cleanup_count} stale cache entries")
294
+
295
+ return CleanupResult(
296
+ status='completed',
297
+ cleaned_entries=cleanup_count,
298
+ cleanup_type='stale_usage_cache'
299
+ )
300
+
301
+ except Exception as e:
302
+ logger.error(f"Cache cleanup failed: {e}")
303
+ return CleanupResult(
304
+ status='error',
305
+ error=str(e),
306
+ cleaned_entries=0,
307
+ cleanup_type='stale_usage_cache'
308
+ )
@@ -0,0 +1,23 @@
1
+ {% comment %}
2
+ Reusable dashboard header component
3
+
4
+ Usage:
5
+ {% include "admin/payments/_components/dashboard_header.html" with title="Payment Overview" icon="πŸ’³" %}
6
+
7
+ Parameters:
8
+ - title: Dashboard title
9
+ - icon: Emoji or icon to display
10
+ - subtitle: Optional subtitle
11
+ {% endcomment %}
12
+
13
+ <div class="bg-white border border-base-200 dark:bg-base-900 dark:border-base-700 p-4 rounded-default mb-4">
14
+ <h3 class="text-lg font-semibold text-font-important-light dark:text-font-important-dark mb-3">
15
+ {% if icon %}{{ icon }} {% endif %}{{ title }}
16
+ {% if subtitle %}
17
+ <span class="text-sm font-normal text-font-subtle-light dark:text-font-subtle-dark">{{ subtitle }}</span>
18
+ {% endif %}
19
+ </h3>
20
+
21
+ {% block dashboard_content %}
22
+ {% endblock %}
23
+ </div>
@@ -0,0 +1,25 @@
1
+ {% comment %}
2
+ Reusable stats card component for payment admin dashboards
3
+
4
+ Usage:
5
+ {% include "admin/payments/_components/stats_card.html" with title="Total Users" value="1,234" color="primary" icon="πŸ‘₯" %}
6
+
7
+ Parameters:
8
+ - title: Card title
9
+ - value: Main value to display
10
+ - color: Color theme (primary, success, warning, error, info)
11
+ - icon: Emoji or icon to display
12
+ - subtitle: Optional subtitle text
13
+ {% endcomment %}
14
+
15
+ {% load humanize %}
16
+
17
+ <div class="text-center bg-base-50 dark:bg-base-800 p-3 rounded-default">
18
+ <p class="text-2xl font-bold text-{{ color|default:'primary' }}-600 dark:text-{{ color|default:'primary' }}-400">
19
+ {% if icon %}<span class="mr-1">{{ icon }}</span>{% endif %}{{ value|default:0 }}
20
+ </p>
21
+ <p class="text-sm text-font-subtle-light dark:text-font-subtle-dark">{{ title }}</p>
22
+ {% if subtitle %}
23
+ <p class="text-xs text-gray-500">{{ subtitle }}</p>
24
+ {% endif %}
25
+ </div>
@@ -0,0 +1,16 @@
1
+ {% comment %}
2
+ Reusable stats grid component for payment admin dashboards
3
+
4
+ Usage:
5
+ {% include "admin/payments/_components/stats_grid.html" with stats=stats_data columns=4 %}
6
+
7
+ Parameters:
8
+ - stats: List of stat objects with title, value, color, icon properties
9
+ - columns: Number of columns (2, 3, 4, 5)
10
+ {% endcomment %}
11
+
12
+ <div class="grid grid-cols-{{ columns|default:4 }} gap-4 mb-4">
13
+ {% for stat in stats %}
14
+ {% include "admin/payments/_components/stats_card.html" with title=stat.title value=stat.value color=stat.color icon=stat.icon subtitle=stat.subtitle %}
15
+ {% endfor %}
16
+ </div>
@@ -0,0 +1,39 @@
1
+ {% extends "admin/change_list.html" %}
2
+ {% load static %}
3
+
4
+ {% block result_list %}
5
+ <!-- API Key Statistics Dashboard -->
6
+ {% include "admin/payments/_components/dashboard_header.html" with title="API Keys Overview" icon="πŸ”‘" %}
7
+
8
+ {% if api_key_stats %}
9
+ {% with stats=api_key_stats %}
10
+ <div class="grid grid-cols-4 gap-4 mb-4">
11
+ {% include "admin/payments/_components/stats_card.html" with title="Total Keys" value=stats.total_keys color="primary" %}
12
+ {% include "admin/payments/_components/stats_card.html" with title="Active" value=stats.active_keys color="success" %}
13
+ {% include "admin/payments/_components/stats_card.html" with title="Expiring Soon" value=stats.expiring_soon color="warning" %}
14
+ {% include "admin/payments/_components/stats_card.html" with title="Expired" value=stats.expired_keys color="error" %}
15
+ </div>
16
+ {% endwith %}
17
+ {% endif %}
18
+
19
+ <div class="grid grid-cols-2 gap-4">
20
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
21
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">Usage Statistics</h4>
22
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
23
+ <li>πŸ“Š Total Requests: <span class="font-medium text-primary-600 dark:text-primary-400">{{ stats.total_requests|default:0 }}</span></li>
24
+ <li>πŸ“ˆ Recently Used: <span class="font-medium text-success-600 dark:text-success-400">{{ stats.recently_used|default:0 }}</span></li>
25
+ <li>πŸ†• Unused: <span class="font-medium text-warning-600 dark:text-warning-400">{{ stats.unused_keys|default:0 }}</span></li>
26
+ </ul>
27
+ </div>
28
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
29
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">πŸ”’ Security</h4>
30
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
31
+ <li>πŸ”₯ Heavy Usage: <span class="font-medium text-purple-600 dark:text-purple-400">{{ stats.heavy_usage_keys|default:0 }}</span></li>
32
+ <li>⚠️ Old Unused: <span class="font-medium text-error-600 dark:text-error-400">{{ stats.never_used_old_keys|default:0 }}</span></li>
33
+ <li>πŸ‘₯ Top Users: <span class="font-medium text-info-600 dark:text-info-400">{{ stats.top_users|length|default:0 }}</span></li>
34
+ </ul>
35
+ </div>
36
+ </div>
37
+
38
+ {{ block.super }}
39
+ {% endblock %}
@@ -0,0 +1,50 @@
1
+ {% extends "admin/change_list.html" %}
2
+ {% load static %}
3
+
4
+ {% block result_list %}
5
+ <!-- Balance Statistics Dashboard -->
6
+ {% if balance_stats %}
7
+ {% include "admin/payments/_components/dashboard_header.html" with title="Balance Overview" icon="πŸ’°" %}
8
+
9
+ {% with stats=balance_stats %}
10
+ <div class="grid grid-cols-4 gap-4 mb-4">
11
+ {% include "admin/payments/_components/stats_card.html" with title="Total Balances" value=stats.total_balances color="primary" %}
12
+ {% include "admin/payments/_components/stats_card.html" with title="Total Value" value="$"|add:stats.total_balance|floatformat:2 color="success" %}
13
+ {% include "admin/payments/_components/stats_card.html" with title="Average Balance" value="$"|add:stats.avg_balance|floatformat:2 color="warning" %}
14
+ {% include "admin/payments/_components/stats_card.html" with title="Active (7 days)" value=stats.active_balances color="info" %}
15
+ </div>
16
+ {% endwith %}
17
+
18
+ <div class="grid grid-cols-3 gap-4">
19
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
20
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">Balance Distribution</h4>
21
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
22
+ <li>πŸ’Έ Empty: <span class="font-medium text-base-600 dark:text-base-400">{{ balance_stats.zero_balances|default:0 }}</span></li>
23
+ <li>πŸͺ™ Low ($0-10): <span class="font-medium text-yellow-600 dark:text-yellow-400">{{ balance_stats.low_balances|default:0 }}</span></li>
24
+ <li>πŸ’° Medium ($10-100): <span class="font-medium text-green-600 dark:text-green-400">{{ balance_stats.medium_balances|default:0 }}</span></li>
25
+ <li>πŸ’Ž High ($100-1000): <span class="font-medium text-blue-600 dark:text-blue-400">{{ balance_stats.high_balances|default:0 }}</span></li>
26
+ <li>πŸ‹ Whale ($1000+): <span class="font-medium text-purple-600 dark:text-purple-400">{{ balance_stats.whale_balances|default:0 }}</span></li>
27
+ </ul>
28
+ </div>
29
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
30
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">🚨 Alerts</h4>
31
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
32
+ <li>⚠️ Negative: <span class="font-medium text-red-600 dark:text-red-400">{{ balance_stats.negative_balances|default:0 }}</span></li>
33
+ <li>πŸ”’ Reserved: <span class="font-medium text-orange-600 dark:text-orange-400">${{ balance_stats.total_reserved|floatformat:2|default:0 }}</span></li>
34
+ </ul>
35
+ </div>
36
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
37
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">πŸ† Top Balances</h4>
38
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
39
+ {% for balance in balance_stats.top_balances %}
40
+ <li><span class="font-semibold text-primary-600 dark:text-primary-400">{{ balance.user.username|truncatechars:15 }}:</span> ${{ balance.balance_usd|floatformat:2 }}</li>
41
+ {% empty %}
42
+ <li class="text-font-subtle-light dark:text-font-subtle-dark">No balances available</li>
43
+ {% endfor %}
44
+ </ul>
45
+ </div>
46
+ </div>
47
+ {% endif %}
48
+
49
+ {{ block.super }}
50
+ {% endblock %}
@@ -0,0 +1,40 @@
1
+ {% extends "admin/change_list.html" %}
2
+ {% load static %}
3
+
4
+ {% block result_list %}
5
+ <!-- Currency Statistics Dashboard -->
6
+ {% if currency_stats %}
7
+ {% include "admin/payments/_components/dashboard_header.html" with title="Currency Overview" icon="πŸ“Š" %}
8
+
9
+ {% with stats=currency_stats %}
10
+ <div class="grid grid-cols-4 gap-4 mb-4">
11
+ {% include "admin/payments/_components/stats_card.html" with title="Total Currencies" value=stats.total_currencies color="primary" %}
12
+ {% include "admin/payments/_components/stats_card.html" with title="Provider Mappings" value=stats.enabled_provider_currencies color="success" %}
13
+ {% include "admin/payments/_components/stats_card.html" with title="With USD Rates" value=stats.currencies_with_rates color="warning" %}
14
+ {% include "admin/payments/_components/stats_card.html" with title="Rate Coverage" value=stats.rate_coverage|floatformat:1|add:"%" color="info" %}
15
+ </div>
16
+ {% endwith %}
17
+
18
+ <div class="grid grid-cols-2 gap-4">
19
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
20
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">Currency Types</h4>
21
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
22
+ <li>πŸ’΅ Fiat: <span class="font-medium text-primary-600 dark:text-primary-400">{{ stats.fiat_count|default:0 }}</span></li>
23
+ <li>β‚Ώ Crypto: <span class="font-medium text-warning-600 dark:text-warning-400">{{ stats.crypto_count|default:0 }}</span></li>
24
+ </ul>
25
+ </div>
26
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
27
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">πŸš€ Top Supported</h4>
28
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
29
+ {% for currency in stats.top_currencies %}
30
+ <li><span class="font-semibold text-primary-600 dark:text-primary-400">{{ currency.code }}:</span> {{ currency.provider_count }} provider{{ currency.provider_count|pluralize }}</li>
31
+ {% empty %}
32
+ <li class="text-font-subtle-light dark:text-font-subtle-dark">No provider data available</li>
33
+ {% endfor %}
34
+ </ul>
35
+ </div>
36
+ </div>
37
+ {% endif %}
38
+
39
+ {{ block.super }}
40
+ {% endblock %}
@@ -0,0 +1,48 @@
1
+ {% extends "admin/change_list.html" %}
2
+ {% load static %}
3
+
4
+ {% block result_list %}
5
+ <!-- Payment Statistics Dashboard -->
6
+ {% if payment_stats %}
7
+ {% include "admin/payments/_components/dashboard_header.html" with title="Payment Overview" icon="πŸ’³" %}
8
+
9
+ {% with stats=payment_stats %}
10
+ <div class="grid grid-cols-4 gap-4 mb-4">
11
+ {% include "admin/payments/_components/stats_card.html" with title="Total Payments" value=stats.total_payments color="primary" %}
12
+ {% include "admin/payments/_components/stats_card.html" with title="Completed" value=stats.status_stats.completed|default:0 color="success" %}
13
+ {% include "admin/payments/_components/stats_card.html" with title="Pending" value=stats.status_stats.pending|default:0 color="warning" %}
14
+ {% include "admin/payments/_components/stats_card.html" with title="Failed" value=stats.status_stats.failed|default:0 color="error" %}
15
+ </div>
16
+
17
+ <div class="grid grid-cols-3 gap-4">
18
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
19
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">πŸ’° Financial</h4>
20
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
21
+ <li>Total Volume: <span class="font-medium text-primary-600 dark:text-primary-400">${{ stats.total_amount|floatformat:2|default:0 }}</span></li>
22
+ <li>Completed: <span class="font-medium text-success-600 dark:text-success-400">${{ stats.completed_amount|floatformat:2|default:0 }}</span></li>
23
+ <li>Success Rate: <span class="font-medium text-info-600 dark:text-info-400">{{ stats.success_rate|floatformat:1|default:0 }}%</span></li>
24
+ </ul>
25
+ </div>
26
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
27
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">πŸ“Š Recent (24h)</h4>
28
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
29
+ <li>Payments: <span class="font-medium text-primary-600 dark:text-primary-400">{{ stats.recent_payments|default:0 }}</span></li>
30
+ <li>Volume: <span class="font-medium text-success-600 dark:text-success-400">${{ stats.recent_amount|floatformat:2|default:0 }}</span></li>
31
+ </ul>
32
+ </div>
33
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
34
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">πŸ”— Providers</h4>
35
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
36
+ {% for provider in stats.provider_stats %}
37
+ <li>{{ provider.provider|title }}: <span class="font-medium text-primary-600 dark:text-primary-400">{{ provider.count }}</span></li>
38
+ {% empty %}
39
+ <li class="text-font-subtle-light dark:text-font-subtle-dark">No provider data</li>
40
+ {% endfor %}
41
+ </ul>
42
+ </div>
43
+ </div>
44
+ {% endwith %}
45
+ {% endif %}
46
+
47
+ {{ block.super }}
48
+ {% endblock %}
@@ -0,0 +1,48 @@
1
+ {% extends "admin/change_list.html" %}
2
+ {% load static %}
3
+
4
+ {% block result_list %}
5
+ <!-- Subscription Statistics Dashboard -->
6
+ {% if subscription_stats %}
7
+ {% include "admin/payments/_components/dashboard_header.html" with title="Subscriptions Overview" icon="πŸ“‹" %}
8
+
9
+ {% with stats=subscription_stats %}
10
+ <div class="grid grid-cols-4 gap-4 mb-4">
11
+ {% include "admin/payments/_components/stats_card.html" with title="Total Subscriptions" value=stats.total_subscriptions color="primary" %}
12
+ {% include "admin/payments/_components/stats_card.html" with title="Active" value=stats.active_subscriptions color="success" %}
13
+ {% include "admin/payments/_components/stats_card.html" with title="Expiring Soon" value=stats.expiring_subscriptions color="warning" %}
14
+ {% include "admin/payments/_components/stats_card.html" with title="Monthly Revenue" value="$"|add:stats.monthly_revenue|floatformat:2 color="info" %}
15
+ </div>
16
+ {% endwith %}
17
+ {% endif %}
18
+
19
+ <div class="grid grid-cols-3 gap-4">
20
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
21
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">Subscription Tiers</h4>
22
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
23
+ <li>πŸ₯‰ Basic: <span class="font-medium text-base-600 dark:text-base-400">{{ stats.basic_subs|default:0 }}</span></li>
24
+ <li>πŸ₯ˆ Premium: <span class="font-medium text-blue-600 dark:text-blue-400">{{ stats.premium_subs|default:0 }}</span></li>
25
+ <li>πŸ₯‡ Enterprise: <span class="font-medium text-purple-600 dark:text-purple-400">{{ stats.enterprise_subs|default:0 }}</span></li>
26
+ </ul>
27
+ </div>
28
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
29
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">Status Distribution</h4>
30
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
31
+ <li>βœ… Active: <span class="font-medium text-success-600 dark:text-success-400">{{ stats.active_count|default:0 }}</span></li>
32
+ <li>⏸️ Paused: <span class="font-medium text-warning-600 dark:text-warning-400">{{ stats.paused_count|default:0 }}</span></li>
33
+ <li>❌ Cancelled: <span class="font-medium text-error-600 dark:text-error-400">{{ stats.cancelled_count|default:0 }}</span></li>
34
+ <li>⏰ Expired: <span class="font-medium text-base-600 dark:text-base-400">{{ stats.expired_count|default:0 }}</span></li>
35
+ </ul>
36
+ </div>
37
+ <div class="bg-base-50 dark:bg-base-800 p-3 rounded-default">
38
+ <h4 class="font-semibold text-font-important-light dark:text-font-important-dark mb-2">πŸ“Š Metrics</h4>
39
+ <ul class="text-sm space-y-1 text-font-default-light dark:text-font-default-dark">
40
+ <li>πŸ“ˆ New (30d): <span class="font-medium text-success-600 dark:text-success-400">{{ stats.new_subs_30d|default:0 }}</span></li>
41
+ <li>πŸ“‰ Churn Rate: <span class="font-medium text-warning-600 dark:text-warning-400">{{ stats.churn_rate|floatformat:1|default:0 }}%</span></li>
42
+ <li>πŸ’° ARPU: <span class="font-medium text-primary-600 dark:text-primary-400">${{ stats.arpu|floatformat:2|default:0 }}</span></li>
43
+ </ul>
44
+ </div>
45
+ </div>
46
+
47
+ {{ block.super }}
48
+ {% endblock %}
@@ -464,3 +464,11 @@ def divide(value, arg):
464
464
  return float(value) / float(arg)
465
465
  except (ValueError, TypeError, ZeroDivisionError):
466
466
  return 0
467
+
468
+
469
+
470
+
471
+
472
+
473
+
474
+
@@ -11,7 +11,7 @@ from rest_framework_nested import routers
11
11
  from .views.api import (
12
12
  PaymentViewSet, UserPaymentViewSet, PaymentCreateView, PaymentStatusView,
13
13
  UserBalanceViewSet, TransactionViewSet, UserTransactionViewSet,
14
- CurrencyViewSet, NetworkViewSet, ProviderCurrencyViewSet, CurrencyConversionView, CurrencyRatesView, SupportedCurrenciesView,
14
+ CurrencyViewSet, NetworkViewSet, ProviderCurrencyViewSet, CurrencyRatesView, SupportedCurrenciesView,
15
15
  SubscriptionViewSet, UserSubscriptionViewSet, EndpointGroupViewSet, TariffViewSet,
16
16
  APIKeyViewSet, UserAPIKeyViewSet, APIKeyCreateView, APIKeyValidateView,
17
17
  UniversalWebhookView, webhook_health_check, webhook_stats, supported_providers,
@@ -60,7 +60,8 @@ urlpatterns = [
60
60
  path('payments/create/', PaymentCreateView.as_view(), name='payment-create'),
61
61
  path('payments/status/<uuid:pk>/', PaymentStatusView.as_view(), name='payment-status'),
62
62
 
63
- path('currencies/convert/', CurrencyConversionView.as_view(), name='currency-convert'),
63
+ # Note: currencies/convert/ is handled by CurrencyViewSet action
64
+ # path('currencies/convert/', CurrencyConversionView.as_view(), name='currency-convert'),
64
65
  path('currencies/rates/', CurrencyRatesView.as_view(), name='currency-rates'),
65
66
  path('currencies/supported/', SupportedCurrenciesView.as_view(), name='currencies-supported'),
66
67
 
@@ -39,7 +39,7 @@ webhook_events_router.register(r'events', AdminWebhookEventViewSet, basename='ad
39
39
 
40
40
  # Public API router (no authentication required)
41
41
  public_router = DefaultRouter()
42
- public_router.register(r'webhooks', WebhookTestViewSet, basename='webhook-test')
42
+ public_router.register(r'webhook-test', WebhookTestViewSet, basename='webhook-test')
43
43
 
44
44
  urlpatterns = [
45
45
  # Template Views