django-cfg 1.2.31__py3-none-any.whl → 1.3.1__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 (256) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/health/views.py +4 -2
  3. django_cfg/apps/knowbase/config/settings.py +16 -15
  4. django_cfg/apps/payments/README.md +326 -0
  5. django_cfg/apps/payments/admin/__init__.py +20 -10
  6. django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
  7. django_cfg/apps/payments/admin/balance_admin.py +592 -297
  8. django_cfg/apps/payments/admin/currencies_admin.py +526 -222
  9. django_cfg/apps/payments/admin/filters.py +306 -199
  10. django_cfg/apps/payments/admin/payments_admin.py +465 -70
  11. django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
  12. django_cfg/apps/payments/admin_interface/__init__.py +18 -0
  13. django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
  14. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
  23. django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
  24. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
  25. django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
  26. django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
  27. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
  28. django_cfg/apps/payments/apps.py +34 -9
  29. django_cfg/apps/payments/config/__init__.py +28 -51
  30. django_cfg/apps/payments/config/constance/__init__.py +22 -0
  31. django_cfg/apps/payments/config/constance/config_service.py +123 -0
  32. django_cfg/apps/payments/config/constance/fields.py +69 -0
  33. django_cfg/apps/payments/config/constance/settings.py +160 -0
  34. django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
  35. django_cfg/apps/payments/config/helpers.py +130 -0
  36. django_cfg/apps/payments/management/__init__.py +1 -3
  37. django_cfg/apps/payments/management/commands/__init__.py +1 -3
  38. django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
  39. django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
  40. django_cfg/apps/payments/middleware/__init__.py +3 -1
  41. django_cfg/apps/payments/middleware/api_access.py +329 -222
  42. django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
  43. django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
  44. django_cfg/apps/payments/migrations/0001_initial.py +708 -536
  45. django_cfg/apps/payments/models/__init__.py +13 -18
  46. django_cfg/apps/payments/models/api_keys.py +121 -43
  47. django_cfg/apps/payments/models/balance.py +150 -115
  48. django_cfg/apps/payments/models/base.py +68 -15
  49. django_cfg/apps/payments/models/currencies.py +172 -148
  50. django_cfg/apps/payments/models/managers/__init__.py +44 -0
  51. django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
  52. django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
  53. django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
  54. django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
  55. django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
  56. django_cfg/apps/payments/models/payments.py +235 -285
  57. django_cfg/apps/payments/models/subscriptions.py +257 -177
  58. django_cfg/apps/payments/models/tariffs.py +147 -40
  59. django_cfg/apps/payments/services/__init__.py +209 -56
  60. django_cfg/apps/payments/services/cache/__init__.py +6 -6
  61. django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
  62. django_cfg/apps/payments/services/core/__init__.py +10 -6
  63. django_cfg/apps/payments/services/core/balance_service.py +435 -360
  64. django_cfg/apps/payments/services/core/base.py +166 -0
  65. django_cfg/apps/payments/services/core/currency_service.py +478 -0
  66. django_cfg/apps/payments/services/core/payment_service.py +346 -467
  67. django_cfg/apps/payments/services/core/subscription_service.py +425 -481
  68. django_cfg/apps/payments/services/core/webhook_service.py +410 -0
  69. django_cfg/apps/payments/services/integrations/__init__.py +29 -0
  70. django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
  71. django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
  72. django_cfg/apps/payments/services/providers/__init__.py +9 -14
  73. django_cfg/apps/payments/services/providers/base.py +234 -174
  74. django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
  75. django_cfg/apps/payments/services/providers/registry.py +367 -301
  76. django_cfg/apps/payments/services/types/__init__.py +78 -0
  77. django_cfg/apps/payments/services/types/data.py +177 -0
  78. django_cfg/apps/payments/services/types/requests.py +150 -0
  79. django_cfg/apps/payments/services/types/responses.py +156 -0
  80. django_cfg/apps/payments/services/types/webhooks.py +232 -0
  81. django_cfg/apps/payments/signals/__init__.py +33 -8
  82. django_cfg/apps/payments/signals/api_key_signals.py +210 -129
  83. django_cfg/apps/payments/signals/balance_signals.py +174 -0
  84. django_cfg/apps/payments/signals/payment_signals.py +128 -103
  85. django_cfg/apps/payments/signals/subscription_signals.py +194 -142
  86. django_cfg/apps/payments/static/payments/css/components.css +380 -0
  87. django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
  88. django_cfg/apps/payments/static/payments/js/components.js +545 -0
  89. django_cfg/apps/payments/static/payments/js/utils.js +412 -0
  90. django_cfg/apps/payments/templatetags/__init__.py +1 -1
  91. django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
  92. django_cfg/apps/payments/urls.py +45 -48
  93. django_cfg/apps/payments/urls_admin.py +33 -42
  94. django_cfg/apps/payments/views/api/__init__.py +101 -0
  95. django_cfg/apps/payments/views/api/api_keys.py +387 -0
  96. django_cfg/apps/payments/views/api/balances.py +381 -0
  97. django_cfg/apps/payments/views/api/base.py +298 -0
  98. django_cfg/apps/payments/views/api/currencies.py +402 -0
  99. django_cfg/apps/payments/views/api/payments.py +415 -0
  100. django_cfg/apps/payments/views/api/subscriptions.py +475 -0
  101. django_cfg/apps/payments/views/api/webhooks.py +476 -0
  102. django_cfg/apps/payments/views/serializers/__init__.py +99 -0
  103. django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
  104. django_cfg/apps/payments/views/serializers/balances.py +300 -0
  105. django_cfg/apps/payments/views/serializers/currencies.py +335 -0
  106. django_cfg/apps/payments/views/serializers/payments.py +387 -0
  107. django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
  108. django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
  109. django_cfg/config.py +1 -1
  110. django_cfg/core/config.py +40 -4
  111. django_cfg/core/generation.py +25 -4
  112. django_cfg/core/integration/README.md +363 -0
  113. django_cfg/core/integration/__init__.py +47 -0
  114. django_cfg/core/integration/commands_collector.py +239 -0
  115. django_cfg/core/integration/display/__init__.py +15 -0
  116. django_cfg/core/integration/display/base.py +157 -0
  117. django_cfg/core/integration/display/ngrok.py +164 -0
  118. django_cfg/core/integration/display/startup.py +815 -0
  119. django_cfg/core/integration/url_integration.py +123 -0
  120. django_cfg/core/integration/version_checker.py +160 -0
  121. django_cfg/management/commands/auto_generate.py +4 -0
  122. django_cfg/management/commands/check_settings.py +6 -0
  123. django_cfg/management/commands/clear_constance.py +5 -2
  124. django_cfg/management/commands/create_token.py +6 -0
  125. django_cfg/management/commands/list_urls.py +6 -0
  126. django_cfg/management/commands/migrate_all.py +6 -0
  127. django_cfg/management/commands/migrator.py +3 -0
  128. django_cfg/management/commands/rundramatiq.py +6 -0
  129. django_cfg/management/commands/runserver_ngrok.py +51 -29
  130. django_cfg/management/commands/script.py +6 -0
  131. django_cfg/management/commands/show_config.py +12 -2
  132. django_cfg/management/commands/show_urls.py +4 -0
  133. django_cfg/management/commands/superuser.py +6 -0
  134. django_cfg/management/commands/task_clear.py +4 -1
  135. django_cfg/management/commands/task_status.py +3 -1
  136. django_cfg/management/commands/test_email.py +3 -0
  137. django_cfg/management/commands/test_telegram.py +6 -0
  138. django_cfg/management/commands/test_twilio.py +6 -0
  139. django_cfg/management/commands/tree.py +6 -0
  140. django_cfg/management/commands/validate_config.py +155 -149
  141. django_cfg/models/constance.py +31 -11
  142. django_cfg/models/payments.py +175 -492
  143. django_cfg/modules/django_logger.py +160 -146
  144. django_cfg/modules/django_unfold/dashboard.py +64 -16
  145. django_cfg/registry/core.py +1 -0
  146. django_cfg/template_archive/django_sample.zip +0 -0
  147. django_cfg/utils/smart_defaults.py +222 -571
  148. django_cfg/utils/toolkit.py +51 -11
  149. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
  150. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
  151. django_cfg/apps/payments/__init__.py +0 -8
  152. django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
  153. django_cfg/apps/payments/config/module.py +0 -70
  154. django_cfg/apps/payments/config/providers.py +0 -105
  155. django_cfg/apps/payments/config/settings.py +0 -96
  156. django_cfg/apps/payments/config/utils.py +0 -52
  157. django_cfg/apps/payments/decorators.py +0 -291
  158. django_cfg/apps/payments/management/commands/README.md +0 -146
  159. django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
  160. django_cfg/apps/payments/managers/__init__.py +0 -23
  161. django_cfg/apps/payments/managers/api_key_manager.py +0 -35
  162. django_cfg/apps/payments/managers/balance_manager.py +0 -361
  163. django_cfg/apps/payments/managers/currency_manager.py +0 -306
  164. django_cfg/apps/payments/managers/payment_manager.py +0 -192
  165. django_cfg/apps/payments/managers/subscription_manager.py +0 -37
  166. django_cfg/apps/payments/managers/tariff_manager.py +0 -29
  167. django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
  168. django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
  169. django_cfg/apps/payments/models/events.py +0 -73
  170. django_cfg/apps/payments/serializers/__init__.py +0 -57
  171. django_cfg/apps/payments/serializers/api_keys.py +0 -51
  172. django_cfg/apps/payments/serializers/balance.py +0 -59
  173. django_cfg/apps/payments/serializers/currencies.py +0 -63
  174. django_cfg/apps/payments/serializers/payments.py +0 -62
  175. django_cfg/apps/payments/serializers/subscriptions.py +0 -71
  176. django_cfg/apps/payments/serializers/tariffs.py +0 -56
  177. django_cfg/apps/payments/services/billing/__init__.py +0 -8
  178. django_cfg/apps/payments/services/cache/base.py +0 -30
  179. django_cfg/apps/payments/services/core/fallback_service.py +0 -432
  180. django_cfg/apps/payments/services/internal_types.py +0 -461
  181. django_cfg/apps/payments/services/middleware/__init__.py +0 -8
  182. django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
  183. django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
  184. django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
  185. django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
  186. django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
  187. django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
  188. django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
  189. django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
  190. django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
  191. django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
  192. django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
  193. django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
  194. django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
  195. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
  196. django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
  197. django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
  198. django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
  199. django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
  200. django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
  201. django_cfg/apps/payments/services/security/__init__.py +0 -34
  202. django_cfg/apps/payments/services/security/error_handler.py +0 -635
  203. django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
  204. django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
  205. django_cfg/apps/payments/static/payments/css/payments.css +0 -340
  206. django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
  207. django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
  208. django_cfg/apps/payments/static/payments/js/theme.js +0 -86
  209. django_cfg/apps/payments/tasks/__init__.py +0 -12
  210. django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
  211. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
  212. django_cfg/apps/payments/templates/payments/base.html +0 -182
  213. django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
  214. django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
  215. django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
  216. django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
  217. django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
  218. django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
  219. django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
  220. django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
  221. django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
  222. django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
  223. django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
  224. django_cfg/apps/payments/templates/payments/stats.html +0 -261
  225. django_cfg/apps/payments/templates/payments/test.html +0 -213
  226. django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
  227. django_cfg/apps/payments/utils/__init__.py +0 -43
  228. django_cfg/apps/payments/utils/billing_utils.py +0 -342
  229. django_cfg/apps/payments/utils/config_utils.py +0 -239
  230. django_cfg/apps/payments/utils/middleware_utils.py +0 -228
  231. django_cfg/apps/payments/utils/validation_utils.py +0 -94
  232. django_cfg/apps/payments/views/__init__.py +0 -63
  233. django_cfg/apps/payments/views/api_key_views.py +0 -164
  234. django_cfg/apps/payments/views/balance_views.py +0 -75
  235. django_cfg/apps/payments/views/currency_views.py +0 -122
  236. django_cfg/apps/payments/views/payment_views.py +0 -149
  237. django_cfg/apps/payments/views/subscription_views.py +0 -135
  238. django_cfg/apps/payments/views/tariff_views.py +0 -131
  239. django_cfg/apps/payments/views/templates/__init__.py +0 -25
  240. django_cfg/apps/payments/views/templates/ajax.py +0 -451
  241. django_cfg/apps/payments/views/templates/base.py +0 -212
  242. django_cfg/apps/payments/views/templates/dashboard.py +0 -60
  243. django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
  244. django_cfg/apps/payments/views/templates/payment_management.py +0 -158
  245. django_cfg/apps/payments/views/templates/qr_code.py +0 -174
  246. django_cfg/apps/payments/views/templates/stats.py +0 -244
  247. django_cfg/apps/payments/views/templates/utils.py +0 -181
  248. django_cfg/apps/payments/views/webhook_views.py +0 -266
  249. django_cfg/apps/payments/viewsets.py +0 -66
  250. django_cfg/core/integration.py +0 -160
  251. django_cfg/template_archive/.gitignore +0 -1
  252. django_cfg/template_archive/__init__.py +0 -0
  253. django_cfg/urls.py +0 -33
  254. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
  255. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
  256. {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,166 @@
1
+ """
2
+ Base service class for the Universal Payment System v2.0.
3
+
4
+ Provides common functionality for all services.
5
+ """
6
+
7
+ from abc import ABC
8
+ from typing import Optional, Dict, Any, Type
9
+ from django.db import transaction
10
+ from django_cfg.modules.django_logger import get_logger
11
+ from ..types import ServiceOperationResult
12
+
13
+
14
+ class BaseService(ABC):
15
+ """
16
+ Base service class with common functionality.
17
+
18
+ Provides logging, error handling, and transaction management.
19
+ """
20
+
21
+ def __init__(self):
22
+ """Initialize base service."""
23
+ self.logger = get_logger(f"services.{self.__class__.__name__.lower()}")
24
+ self._cache = {}
25
+
26
+ # Initialize config service
27
+ from ...config.constance import get_payment_config_service
28
+ self.config_service = get_payment_config_service()
29
+
30
+ def _create_success_result(
31
+ self,
32
+ message: str = "Operation completed successfully",
33
+ data: Optional[Dict[str, Any]] = None
34
+ ) -> ServiceOperationResult:
35
+ """Create success result."""
36
+ return ServiceOperationResult(
37
+ success=True,
38
+ message=message,
39
+ data=data or {}
40
+ )
41
+
42
+ def _create_error_result(
43
+ self,
44
+ message: str,
45
+ error_code: Optional[str] = None,
46
+ data: Optional[Dict[str, Any]] = None
47
+ ) -> ServiceOperationResult:
48
+ """Create error result."""
49
+ return ServiceOperationResult(
50
+ success=False,
51
+ message=message,
52
+ error_code=error_code,
53
+ data=data or {}
54
+ )
55
+
56
+ def _log_operation(
57
+ self,
58
+ operation: str,
59
+ success: bool,
60
+ **kwargs
61
+ ) -> None:
62
+ """Log service operation."""
63
+ log_data = {
64
+ 'service': self.__class__.__name__,
65
+ 'operation': operation,
66
+ 'success': success,
67
+ **kwargs
68
+ }
69
+
70
+ if success:
71
+ self.logger.info(f"Operation {operation} completed successfully", extra=log_data)
72
+ else:
73
+ self.logger.error(f"Operation {operation} failed", extra=log_data)
74
+
75
+ def _handle_exception(
76
+ self,
77
+ operation: str,
78
+ exception: Exception,
79
+ **context
80
+ ) -> ServiceOperationResult:
81
+ """Handle service exception."""
82
+ error_message = f"Service error in {operation}: {str(exception)}"
83
+
84
+ self.logger.error(error_message, extra={
85
+ 'service': self.__class__.__name__,
86
+ 'operation': operation,
87
+ 'exception_type': type(exception).__name__,
88
+ 'exception_message': str(exception),
89
+ **context
90
+ }, exc_info=True)
91
+
92
+ return self._create_error_result(
93
+ message=error_message,
94
+ error_code=type(exception).__name__.lower(),
95
+ data={'context': context}
96
+ )
97
+
98
+ @transaction.atomic
99
+ def _execute_with_transaction(self, operation_func, *args, **kwargs):
100
+ """Execute operation within database transaction."""
101
+ try:
102
+ return operation_func(*args, **kwargs)
103
+ except Exception as e:
104
+ self.logger.error(f"Transaction rolled back due to error: {e}")
105
+ raise
106
+
107
+ def _validate_input(self, data: Any, model_class: Type) -> Any:
108
+ """Validate input data using Pydantic model."""
109
+ try:
110
+ if isinstance(data, dict):
111
+ return model_class(**data)
112
+ elif isinstance(data, model_class):
113
+ return data
114
+ else:
115
+ return model_class.model_validate(data)
116
+ except Exception as e:
117
+ raise ValueError(f"Invalid input data: {e}")
118
+
119
+ def _get_cache_key(self, prefix: str, *args) -> str:
120
+ """Generate cache key."""
121
+ key_parts = [prefix] + [str(arg) for arg in args]
122
+ return ":".join(key_parts)
123
+
124
+ def _cache_get(self, key: str) -> Optional[Any]:
125
+ """Get value from cache."""
126
+ return self._cache.get(key)
127
+
128
+ def _cache_set(self, key: str, value: Any, ttl: int = 300) -> None:
129
+ """Set value in cache."""
130
+ # Simple in-memory cache for now
131
+ # In production, this would use Redis
132
+ self._cache[key] = value
133
+
134
+ def _cache_delete(self, key: str) -> None:
135
+ """Delete value from cache."""
136
+ self._cache.pop(key, None)
137
+
138
+ def _cache_clear(self, prefix: Optional[str] = None) -> None:
139
+ """Clear cache entries."""
140
+ if prefix:
141
+ keys_to_delete = [k for k in self._cache.keys() if k.startswith(prefix)]
142
+ for key in keys_to_delete:
143
+ del self._cache[key]
144
+ else:
145
+ self._cache.clear()
146
+
147
+ def get_service_stats(self) -> Dict[str, Any]:
148
+ """Get service statistics."""
149
+ return {
150
+ 'service_name': self.__class__.__name__,
151
+ 'cache_size': len(self._cache),
152
+ 'cache_keys': list(self._cache.keys())
153
+ }
154
+
155
+ def health_check(self) -> ServiceOperationResult:
156
+ """Perform service health check."""
157
+ try:
158
+ # Basic health check - can be overridden by subclasses
159
+ stats = self.get_service_stats()
160
+
161
+ return self._create_success_result(
162
+ message=f"{self.__class__.__name__} is healthy",
163
+ data=stats
164
+ )
165
+ except Exception as e:
166
+ return self._handle_exception("health_check", e)
@@ -0,0 +1,478 @@
1
+ """
2
+ Currency service for the Universal Payment System v2.0.
3
+
4
+ Handles currency conversion and rate management using django_currency module.
5
+ """
6
+
7
+ from typing import Optional, Dict, Any, List
8
+ from decimal import Decimal
9
+ from django.utils import timezone
10
+ from datetime import timedelta
11
+
12
+ from .base import BaseService
13
+ from ..types import (
14
+ CurrencyConversionRequest, CurrencyConversionResult, CurrencyData,
15
+ ServiceOperationResult
16
+ )
17
+ from ...models import Currency, ProviderCurrency, Network
18
+ from django_cfg.modules.django_currency import (
19
+ convert_currency, get_exchange_rate, CurrencyError
20
+ )
21
+
22
+
23
+ class CurrencyService(BaseService):
24
+ """
25
+ Currency service with conversion and rate management.
26
+
27
+ Integrates with django_currency module for real-time rates.
28
+ """
29
+
30
+ def convert_currency(self, request: CurrencyConversionRequest) -> CurrencyConversionResult:
31
+ """
32
+ Convert amount between currencies.
33
+
34
+ Args:
35
+ request: Currency conversion request
36
+
37
+ Returns:
38
+ CurrencyConversionResult: Conversion result with rate
39
+ """
40
+ try:
41
+ # Validate request
42
+ if isinstance(request, dict):
43
+ request = CurrencyConversionRequest(**request)
44
+
45
+ self.logger.debug("Converting currency", extra={
46
+ 'amount': request.amount,
47
+ 'from_currency': request.from_currency,
48
+ 'to_currency': request.to_currency
49
+ })
50
+
51
+ # Check if currencies are supported
52
+ validation_result = self._validate_currencies(
53
+ request.from_currency,
54
+ request.to_currency
55
+ )
56
+ if not validation_result.success:
57
+ return CurrencyConversionResult(
58
+ success=False,
59
+ message=validation_result.message,
60
+ error_code=validation_result.error_code
61
+ )
62
+
63
+ # Perform conversion using django_currency
64
+ try:
65
+ converted_amount = convert_currency(
66
+ request.amount,
67
+ request.from_currency,
68
+ request.to_currency
69
+ )
70
+
71
+ exchange_rate = get_exchange_rate(
72
+ request.from_currency,
73
+ request.to_currency
74
+ )
75
+
76
+ self._log_operation(
77
+ "convert_currency",
78
+ True,
79
+ from_currency=request.from_currency,
80
+ to_currency=request.to_currency,
81
+ amount=request.amount,
82
+ converted_amount=converted_amount,
83
+ exchange_rate=exchange_rate
84
+ )
85
+
86
+ return CurrencyConversionResult(
87
+ success=True,
88
+ message="Currency converted successfully",
89
+ amount=request.amount,
90
+ from_currency=request.from_currency,
91
+ to_currency=request.to_currency,
92
+ converted_amount=converted_amount,
93
+ exchange_rate=exchange_rate,
94
+ rate_timestamp=timezone.now()
95
+ )
96
+
97
+ except CurrencyError as e:
98
+ return CurrencyConversionResult(
99
+ success=False,
100
+ message=f"Currency conversion failed: {e}",
101
+ error_code="conversion_failed",
102
+ amount=request.amount,
103
+ from_currency=request.from_currency,
104
+ to_currency=request.to_currency
105
+ )
106
+
107
+ except Exception as e:
108
+ return CurrencyConversionResult(**self._handle_exception(
109
+ "convert_currency", e,
110
+ from_currency=request.from_currency if hasattr(request, 'from_currency') else None,
111
+ to_currency=request.to_currency if hasattr(request, 'to_currency') else None
112
+ ).model_dump())
113
+
114
+ def get_exchange_rate(self, base_currency: str, quote_currency: str) -> ServiceOperationResult:
115
+ """
116
+ Get current exchange rate between currencies.
117
+
118
+ Args:
119
+ base_currency: Base currency code
120
+ quote_currency: Quote currency code
121
+
122
+ Returns:
123
+ ServiceOperationResult: Exchange rate information
124
+ """
125
+ try:
126
+ self.logger.debug("Getting exchange rate", extra={
127
+ 'base_currency': base_currency,
128
+ 'quote_currency': quote_currency
129
+ })
130
+
131
+ # Validate currencies
132
+ validation_result = self._validate_currencies(base_currency, quote_currency)
133
+ if not validation_result.success:
134
+ return validation_result
135
+
136
+ # Get rate using django_currency
137
+ try:
138
+ rate = get_exchange_rate(base_currency, quote_currency)
139
+
140
+ return self._create_success_result(
141
+ "Exchange rate retrieved successfully",
142
+ {
143
+ 'base_currency': base_currency,
144
+ 'quote_currency': quote_currency,
145
+ 'exchange_rate': rate,
146
+ 'rate_timestamp': timezone.now().isoformat(),
147
+ 'pair': f"{base_currency}/{quote_currency}"
148
+ }
149
+ )
150
+
151
+ except CurrencyError as e:
152
+ return self._create_error_result(
153
+ f"Failed to get exchange rate: {e}",
154
+ "rate_fetch_failed"
155
+ )
156
+
157
+ except Exception as e:
158
+ return self._handle_exception(
159
+ "get_exchange_rate", e,
160
+ base_currency=base_currency,
161
+ quote_currency=quote_currency
162
+ )
163
+
164
+ def get_supported_currencies(self, provider: Optional[str] = None) -> ServiceOperationResult:
165
+ """
166
+ Get list of supported currencies.
167
+
168
+ Args:
169
+ provider: Filter by provider (optional)
170
+
171
+ Returns:
172
+ ServiceOperationResult: List of supported currencies
173
+ """
174
+ try:
175
+ self.logger.debug("Getting supported currencies", extra={
176
+ 'provider': provider
177
+ })
178
+
179
+ # Get currencies from database
180
+ queryset = Currency.objects.filter(is_enabled=True)
181
+
182
+ if provider:
183
+ # Filter by provider support
184
+ queryset = queryset.filter(
185
+ providercurrency__provider=provider,
186
+ providercurrency__is_enabled=True
187
+ ).distinct()
188
+
189
+ currencies = queryset.order_by('code')
190
+
191
+ # Convert to data
192
+ currency_data = []
193
+ for currency in currencies:
194
+ data = CurrencyData.model_validate(currency)
195
+ currency_info = data.model_dump()
196
+
197
+ # Add provider-specific info if requested
198
+ if provider:
199
+ try:
200
+ provider_currency = ProviderCurrency.objects.get(
201
+ currency=currency,
202
+ provider=provider,
203
+ is_enabled=True
204
+ )
205
+ currency_info['provider_info'] = {
206
+ 'min_amount': float(provider_currency.min_amount) if provider_currency.min_amount else None,
207
+ 'max_amount': float(provider_currency.max_amount) if provider_currency.max_amount else None,
208
+ 'network_fee': float(provider_currency.network_fee) if provider_currency.network_fee else None,
209
+ 'confirmation_blocks': provider_currency.confirmation_blocks
210
+ }
211
+ except ProviderCurrency.DoesNotExist:
212
+ pass
213
+
214
+ currency_data.append(currency_info)
215
+
216
+ return self._create_success_result(
217
+ f"Retrieved {len(currency_data)} supported currencies",
218
+ {
219
+ 'currencies': currency_data,
220
+ 'count': len(currency_data),
221
+ 'provider': provider
222
+ }
223
+ )
224
+
225
+ except Exception as e:
226
+ return self._handle_exception(
227
+ "get_supported_currencies", e,
228
+ provider=provider
229
+ )
230
+
231
+ def get_currency_networks(self, currency_code: str) -> ServiceOperationResult:
232
+ """
233
+ Get available networks for a currency.
234
+
235
+ Args:
236
+ currency_code: Currency code
237
+
238
+ Returns:
239
+ ServiceOperationResult: Available networks
240
+ """
241
+ try:
242
+ self.logger.debug("Getting currency networks", extra={
243
+ 'currency_code': currency_code
244
+ })
245
+
246
+ # Get currency
247
+ try:
248
+ currency = Currency.objects.get(code=currency_code, is_enabled=True)
249
+ except Currency.DoesNotExist:
250
+ return self._create_error_result(
251
+ f"Currency {currency_code} not found or disabled",
252
+ "currency_not_found"
253
+ )
254
+
255
+ # Get networks
256
+ networks = Network.objects.filter(
257
+ currency_code=currency_code,
258
+ is_enabled=True
259
+ ).order_by('name')
260
+
261
+ network_data = []
262
+ for network in networks:
263
+ network_info = {
264
+ 'code': network.code,
265
+ 'name': network.name,
266
+ 'currency_code': network.currency_code,
267
+ 'is_testnet': network.is_testnet,
268
+ 'confirmation_blocks': network.confirmation_blocks,
269
+ 'block_time_seconds': network.block_time_seconds,
270
+ 'estimated_confirmation_time': network.estimated_confirmation_time()
271
+ }
272
+ network_data.append(network_info)
273
+
274
+ return self._create_success_result(
275
+ f"Retrieved {len(network_data)} networks for {currency_code}",
276
+ {
277
+ 'currency_code': currency_code,
278
+ 'networks': network_data,
279
+ 'count': len(network_data)
280
+ }
281
+ )
282
+
283
+ except Exception as e:
284
+ return self._handle_exception(
285
+ "get_currency_networks", e,
286
+ currency_code=currency_code
287
+ )
288
+
289
+ def update_currency_rates(self, currency_codes: Optional[List[str]] = None) -> ServiceOperationResult:
290
+ """
291
+ Update currency rates from external sources.
292
+
293
+ Args:
294
+ currency_codes: Specific currencies to update (optional)
295
+
296
+ Returns:
297
+ ServiceOperationResult: Update result
298
+ """
299
+ try:
300
+ self.logger.info("Updating currency rates", extra={
301
+ 'currency_codes': currency_codes
302
+ })
303
+
304
+ # Get currencies to update
305
+ if currency_codes:
306
+ currencies = Currency.objects.filter(
307
+ code__in=currency_codes,
308
+ is_enabled=True
309
+ )
310
+ else:
311
+ currencies = Currency.objects.filter(is_enabled=True)
312
+
313
+ updated_count = 0
314
+ failed_count = 0
315
+ errors = []
316
+
317
+ # Update rates for each currency against USD
318
+ for currency in currencies:
319
+ try:
320
+ if currency.code != 'USD':
321
+ # Test rate fetch
322
+ rate = get_exchange_rate('USD', currency.code)
323
+ updated_count += 1
324
+
325
+ self.logger.debug(f"Updated rate for {currency.code}", extra={
326
+ 'currency_code': currency.code,
327
+ 'usd_rate': rate
328
+ })
329
+ except CurrencyError as e:
330
+ failed_count += 1
331
+ error_msg = f"{currency.code}: {str(e)}"
332
+ errors.append(error_msg)
333
+
334
+ self.logger.warning(f"Failed to update rate for {currency.code}", extra={
335
+ 'currency_code': currency.code,
336
+ 'error': str(e)
337
+ })
338
+
339
+ self._log_operation(
340
+ "update_currency_rates",
341
+ failed_count == 0,
342
+ updated_count=updated_count,
343
+ failed_count=failed_count
344
+ )
345
+
346
+ return self._create_success_result(
347
+ f"Updated rates for {updated_count} currencies, {failed_count} failed",
348
+ {
349
+ 'updated_count': updated_count,
350
+ 'failed_count': failed_count,
351
+ 'errors': errors,
352
+ 'total_currencies': currencies.count()
353
+ }
354
+ )
355
+
356
+ except Exception as e:
357
+ return self._handle_exception(
358
+ "update_currency_rates", e,
359
+ currency_codes=currency_codes
360
+ )
361
+
362
+ def get_currency_stats(self) -> ServiceOperationResult:
363
+ """
364
+ Get currency statistics.
365
+
366
+ Returns:
367
+ ServiceOperationResult: Currency statistics
368
+ """
369
+ try:
370
+ # Currency counts
371
+ total_currencies = Currency.objects.count()
372
+ enabled_currencies = Currency.objects.filter(is_enabled=True).count()
373
+ crypto_currencies = Currency.objects.filter(
374
+ currency_type=Currency.CurrencyType.CRYPTO,
375
+ is_enabled=True
376
+ ).count()
377
+ fiat_currencies = Currency.objects.filter(
378
+ currency_type=Currency.CurrencyType.FIAT,
379
+ is_enabled=True
380
+ ).count()
381
+
382
+ # Provider support
383
+ provider_stats = ProviderCurrency.objects.filter(
384
+ is_enabled=True
385
+ ).values('provider').annotate(
386
+ currency_count=models.Count('currency', distinct=True)
387
+ ).order_by('-currency_count')
388
+
389
+ # Network stats
390
+ network_stats = Network.objects.filter(
391
+ is_enabled=True
392
+ ).values('currency_code').annotate(
393
+ network_count=models.Count('id')
394
+ ).order_by('-network_count')
395
+
396
+ stats = {
397
+ 'total_currencies': total_currencies,
398
+ 'enabled_currencies': enabled_currencies,
399
+ 'crypto_currencies': crypto_currencies,
400
+ 'fiat_currencies': fiat_currencies,
401
+ 'provider_support': list(provider_stats),
402
+ 'network_support': list(network_stats),
403
+ 'generated_at': timezone.now().isoformat()
404
+ }
405
+
406
+ return self._create_success_result(
407
+ "Currency statistics retrieved",
408
+ stats
409
+ )
410
+
411
+ except Exception as e:
412
+ return self._handle_exception("get_currency_stats", e)
413
+
414
+ def _validate_currencies(self, from_currency: str, to_currency: str) -> ServiceOperationResult:
415
+ """Validate that currencies are supported."""
416
+ try:
417
+ # Check from_currency
418
+ if not Currency.objects.filter(code=from_currency, is_enabled=True).exists():
419
+ return self._create_error_result(
420
+ f"Currency {from_currency} not supported",
421
+ "from_currency_not_supported"
422
+ )
423
+
424
+ # Check to_currency
425
+ if not Currency.objects.filter(code=to_currency, is_enabled=True).exists():
426
+ return self._create_error_result(
427
+ f"Currency {to_currency} not supported",
428
+ "to_currency_not_supported"
429
+ )
430
+
431
+ return self._create_success_result("Currencies are valid")
432
+
433
+ except Exception as e:
434
+ return self._create_error_result(
435
+ f"Currency validation error: {e}",
436
+ "validation_error"
437
+ )
438
+
439
+ def health_check(self) -> ServiceOperationResult:
440
+ """Perform currency service health check."""
441
+ try:
442
+ # Check database connectivity
443
+ currency_count = Currency.objects.filter(is_enabled=True).count()
444
+
445
+ # Test currency conversion
446
+ try:
447
+ test_rate = get_exchange_rate('USD', 'BTC')
448
+ conversion_healthy = True
449
+ except CurrencyError:
450
+ conversion_healthy = False
451
+
452
+ # Check provider currencies
453
+ provider_currency_count = ProviderCurrency.objects.filter(
454
+ is_enabled=True
455
+ ).count()
456
+
457
+ stats = {
458
+ 'service_name': 'CurrencyService',
459
+ 'enabled_currencies': currency_count,
460
+ 'provider_currencies': provider_currency_count,
461
+ 'conversion_service_healthy': conversion_healthy,
462
+ 'django_currency_module': 'available'
463
+ }
464
+
465
+ if conversion_healthy and currency_count > 0:
466
+ return self._create_success_result(
467
+ "CurrencyService is healthy",
468
+ stats
469
+ )
470
+ else:
471
+ return self._create_error_result(
472
+ "CurrencyService has issues",
473
+ "service_unhealthy",
474
+ stats
475
+ )
476
+
477
+ except Exception as e:
478
+ return self._handle_exception("health_check", e)