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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/health/views.py +4 -2
- django_cfg/apps/knowbase/config/settings.py +16 -15
- django_cfg/apps/payments/README.md +326 -0
- django_cfg/apps/payments/admin/__init__.py +20 -10
- django_cfg/apps/payments/admin/api_keys_admin.py +521 -237
- django_cfg/apps/payments/admin/balance_admin.py +592 -297
- django_cfg/apps/payments/admin/currencies_admin.py +526 -222
- django_cfg/apps/payments/admin/filters.py +306 -199
- django_cfg/apps/payments/admin/payments_admin.py +465 -70
- django_cfg/apps/payments/admin/subscriptions_admin.py +578 -128
- django_cfg/apps/payments/admin_interface/__init__.py +18 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +162 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +38 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/loading_spinner.html +16 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/notification.html +27 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/provider_card.html +86 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +39 -0
- django_cfg/apps/payments/admin_interface/templates/payments/currency_converter.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +300 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/templates/payments/payment_status.html +500 -0
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +594 -0
- django_cfg/apps/payments/admin_interface/views/__init__.py +23 -0
- django_cfg/apps/payments/admin_interface/views/payment_views.py +259 -0
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +37 -0
- django_cfg/apps/payments/apps.py +34 -9
- django_cfg/apps/payments/config/__init__.py +28 -51
- django_cfg/apps/payments/config/constance/__init__.py +22 -0
- django_cfg/apps/payments/config/constance/config_service.py +123 -0
- django_cfg/apps/payments/config/constance/fields.py +69 -0
- django_cfg/apps/payments/config/constance/settings.py +160 -0
- django_cfg/apps/payments/config/django_cfg_integration.py +202 -0
- django_cfg/apps/payments/config/helpers.py +130 -0
- django_cfg/apps/payments/management/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/__init__.py +1 -3
- django_cfg/apps/payments/management/commands/manage_currencies.py +303 -151
- django_cfg/apps/payments/management/commands/manage_providers.py +333 -160
- django_cfg/apps/payments/middleware/__init__.py +3 -1
- django_cfg/apps/payments/middleware/api_access.py +329 -222
- django_cfg/apps/payments/middleware/rate_limiting.py +342 -152
- django_cfg/apps/payments/middleware/usage_tracking.py +249 -240
- django_cfg/apps/payments/migrations/0001_initial.py +708 -536
- django_cfg/apps/payments/models/__init__.py +13 -18
- django_cfg/apps/payments/models/api_keys.py +121 -43
- django_cfg/apps/payments/models/balance.py +150 -115
- django_cfg/apps/payments/models/base.py +68 -15
- django_cfg/apps/payments/models/currencies.py +172 -148
- django_cfg/apps/payments/models/managers/__init__.py +44 -0
- django_cfg/apps/payments/models/managers/api_key_managers.py +329 -0
- django_cfg/apps/payments/models/managers/balance_managers.py +599 -0
- django_cfg/apps/payments/models/managers/currency_managers.py +385 -0
- django_cfg/apps/payments/models/managers/payment_managers.py +511 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +641 -0
- django_cfg/apps/payments/models/payments.py +235 -285
- django_cfg/apps/payments/models/subscriptions.py +257 -177
- django_cfg/apps/payments/models/tariffs.py +147 -40
- django_cfg/apps/payments/services/__init__.py +209 -56
- django_cfg/apps/payments/services/cache/__init__.py +6 -6
- django_cfg/apps/payments/services/cache/{simple_cache.py → cache_service.py} +112 -12
- django_cfg/apps/payments/services/core/__init__.py +10 -6
- django_cfg/apps/payments/services/core/balance_service.py +435 -360
- django_cfg/apps/payments/services/core/base.py +166 -0
- django_cfg/apps/payments/services/core/currency_service.py +478 -0
- django_cfg/apps/payments/services/core/payment_service.py +346 -467
- django_cfg/apps/payments/services/core/subscription_service.py +425 -481
- django_cfg/apps/payments/services/core/webhook_service.py +410 -0
- django_cfg/apps/payments/services/integrations/__init__.py +29 -0
- django_cfg/apps/payments/services/integrations/ngrok_service.py +47 -0
- django_cfg/apps/payments/services/integrations/providers_config.py +107 -0
- django_cfg/apps/payments/services/providers/__init__.py +9 -14
- django_cfg/apps/payments/services/providers/base.py +234 -174
- django_cfg/apps/payments/services/providers/nowpayments.py +478 -0
- django_cfg/apps/payments/services/providers/registry.py +367 -301
- django_cfg/apps/payments/services/types/__init__.py +78 -0
- django_cfg/apps/payments/services/types/data.py +177 -0
- django_cfg/apps/payments/services/types/requests.py +150 -0
- django_cfg/apps/payments/services/types/responses.py +156 -0
- django_cfg/apps/payments/services/types/webhooks.py +232 -0
- django_cfg/apps/payments/signals/__init__.py +33 -8
- django_cfg/apps/payments/signals/api_key_signals.py +210 -129
- django_cfg/apps/payments/signals/balance_signals.py +174 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -103
- django_cfg/apps/payments/signals/subscription_signals.py +194 -142
- django_cfg/apps/payments/static/payments/css/components.css +380 -0
- django_cfg/apps/payments/static/payments/css/dashboard.css +188 -0
- django_cfg/apps/payments/static/payments/js/components.js +545 -0
- django_cfg/apps/payments/static/payments/js/utils.js +412 -0
- django_cfg/apps/payments/templatetags/__init__.py +1 -1
- django_cfg/apps/payments/templatetags/payment_tags.py +466 -0
- django_cfg/apps/payments/urls.py +45 -48
- django_cfg/apps/payments/urls_admin.py +33 -42
- django_cfg/apps/payments/views/api/__init__.py +101 -0
- django_cfg/apps/payments/views/api/api_keys.py +387 -0
- django_cfg/apps/payments/views/api/balances.py +381 -0
- django_cfg/apps/payments/views/api/base.py +298 -0
- django_cfg/apps/payments/views/api/currencies.py +402 -0
- django_cfg/apps/payments/views/api/payments.py +415 -0
- django_cfg/apps/payments/views/api/subscriptions.py +475 -0
- django_cfg/apps/payments/views/api/webhooks.py +476 -0
- django_cfg/apps/payments/views/serializers/__init__.py +99 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +424 -0
- django_cfg/apps/payments/views/serializers/balances.py +300 -0
- django_cfg/apps/payments/views/serializers/currencies.py +335 -0
- django_cfg/apps/payments/views/serializers/payments.py +387 -0
- django_cfg/apps/payments/views/serializers/subscriptions.py +429 -0
- django_cfg/apps/payments/views/serializers/webhooks.py +137 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +40 -4
- django_cfg/core/generation.py +25 -4
- django_cfg/core/integration/README.md +363 -0
- django_cfg/core/integration/__init__.py +47 -0
- django_cfg/core/integration/commands_collector.py +239 -0
- django_cfg/core/integration/display/__init__.py +15 -0
- django_cfg/core/integration/display/base.py +157 -0
- django_cfg/core/integration/display/ngrok.py +164 -0
- django_cfg/core/integration/display/startup.py +815 -0
- django_cfg/core/integration/url_integration.py +123 -0
- django_cfg/core/integration/version_checker.py +160 -0
- django_cfg/management/commands/auto_generate.py +4 -0
- django_cfg/management/commands/check_settings.py +6 -0
- django_cfg/management/commands/clear_constance.py +5 -2
- django_cfg/management/commands/create_token.py +6 -0
- django_cfg/management/commands/list_urls.py +6 -0
- django_cfg/management/commands/migrate_all.py +6 -0
- django_cfg/management/commands/migrator.py +3 -0
- django_cfg/management/commands/rundramatiq.py +6 -0
- django_cfg/management/commands/runserver_ngrok.py +51 -29
- django_cfg/management/commands/script.py +6 -0
- django_cfg/management/commands/show_config.py +12 -2
- django_cfg/management/commands/show_urls.py +4 -0
- django_cfg/management/commands/superuser.py +6 -0
- django_cfg/management/commands/task_clear.py +4 -1
- django_cfg/management/commands/task_status.py +3 -1
- django_cfg/management/commands/test_email.py +3 -0
- django_cfg/management/commands/test_telegram.py +6 -0
- django_cfg/management/commands/test_twilio.py +6 -0
- django_cfg/management/commands/tree.py +6 -0
- django_cfg/management/commands/validate_config.py +155 -149
- django_cfg/models/constance.py +31 -11
- django_cfg/models/payments.py +175 -492
- django_cfg/modules/django_logger.py +160 -146
- django_cfg/modules/django_unfold/dashboard.py +64 -16
- django_cfg/registry/core.py +1 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/utils/smart_defaults.py +222 -571
- django_cfg/utils/toolkit.py +51 -11
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/METADATA +4 -1
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/RECORD +153 -185
- django_cfg/apps/payments/__init__.py +0 -8
- django_cfg/apps/payments/admin/tariffs_admin.py +0 -199
- django_cfg/apps/payments/config/module.py +0 -70
- django_cfg/apps/payments/config/providers.py +0 -105
- django_cfg/apps/payments/config/settings.py +0 -96
- django_cfg/apps/payments/config/utils.py +0 -52
- django_cfg/apps/payments/decorators.py +0 -291
- django_cfg/apps/payments/management/commands/README.md +0 -146
- django_cfg/apps/payments/management/commands/currency_stats.py +0 -304
- django_cfg/apps/payments/managers/__init__.py +0 -23
- django_cfg/apps/payments/managers/api_key_manager.py +0 -35
- django_cfg/apps/payments/managers/balance_manager.py +0 -361
- django_cfg/apps/payments/managers/currency_manager.py +0 -306
- django_cfg/apps/payments/managers/payment_manager.py +0 -192
- django_cfg/apps/payments/managers/subscription_manager.py +0 -37
- django_cfg/apps/payments/managers/tariff_manager.py +0 -29
- django_cfg/apps/payments/migrations/0002_network_providercurrency_and_more.py +0 -241
- django_cfg/apps/payments/migrations/0003_add_usd_rate_cache.py +0 -30
- django_cfg/apps/payments/models/events.py +0 -73
- django_cfg/apps/payments/serializers/__init__.py +0 -57
- django_cfg/apps/payments/serializers/api_keys.py +0 -51
- django_cfg/apps/payments/serializers/balance.py +0 -59
- django_cfg/apps/payments/serializers/currencies.py +0 -63
- django_cfg/apps/payments/serializers/payments.py +0 -62
- django_cfg/apps/payments/serializers/subscriptions.py +0 -71
- django_cfg/apps/payments/serializers/tariffs.py +0 -56
- django_cfg/apps/payments/services/billing/__init__.py +0 -8
- django_cfg/apps/payments/services/cache/base.py +0 -30
- django_cfg/apps/payments/services/core/fallback_service.py +0 -432
- django_cfg/apps/payments/services/internal_types.py +0 -461
- django_cfg/apps/payments/services/middleware/__init__.py +0 -8
- django_cfg/apps/payments/services/monitoring/__init__.py +0 -22
- django_cfg/apps/payments/services/monitoring/api_schemas.py +0 -76
- django_cfg/apps/payments/services/monitoring/provider_health.py +0 -372
- django_cfg/apps/payments/services/providers/cryptapi/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptapi/config.py +0 -8
- django_cfg/apps/payments/services/providers/cryptapi/models.py +0 -192
- django_cfg/apps/payments/services/providers/cryptapi/provider.py +0 -439
- django_cfg/apps/payments/services/providers/cryptomus/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/cryptomus/models.py +0 -176
- django_cfg/apps/payments/services/providers/cryptomus/provider.py +0 -429
- django_cfg/apps/payments/services/providers/cryptomus/provider_v2.py +0 -564
- django_cfg/apps/payments/services/providers/models/__init__.py +0 -34
- django_cfg/apps/payments/services/providers/models/currencies.py +0 -190
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/nowpayments/models.py +0 -196
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +0 -380
- django_cfg/apps/payments/services/providers/stripe/__init__.py +0 -4
- django_cfg/apps/payments/services/providers/stripe/models.py +0 -184
- django_cfg/apps/payments/services/providers/stripe/provider.py +0 -109
- django_cfg/apps/payments/services/security/__init__.py +0 -34
- django_cfg/apps/payments/services/security/error_handler.py +0 -635
- django_cfg/apps/payments/services/security/payment_notifications.py +0 -342
- django_cfg/apps/payments/services/security/webhook_validator.py +0 -474
- django_cfg/apps/payments/static/payments/css/payments.css +0 -340
- django_cfg/apps/payments/static/payments/js/notifications.js +0 -202
- django_cfg/apps/payments/static/payments/js/payment-utils.js +0 -318
- django_cfg/apps/payments/static/payments/js/theme.js +0 -86
- django_cfg/apps/payments/tasks/__init__.py +0 -12
- django_cfg/apps/payments/tasks/webhook_processing.py +0 -177
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +0 -50
- django_cfg/apps/payments/templates/payments/base.html +0 -182
- django_cfg/apps/payments/templates/payments/components/payment_card.html +0 -201
- django_cfg/apps/payments/templates/payments/components/payment_qr_code.html +0 -109
- django_cfg/apps/payments/templates/payments/components/progress_bar.html +0 -43
- django_cfg/apps/payments/templates/payments/components/provider_stats.html +0 -40
- django_cfg/apps/payments/templates/payments/components/status_badge.html +0 -34
- django_cfg/apps/payments/templates/payments/components/status_overview.html +0 -148
- django_cfg/apps/payments/templates/payments/dashboard.html +0 -258
- django_cfg/apps/payments/templates/payments/dashboard_simple_test.html +0 -35
- django_cfg/apps/payments/templates/payments/payment_create.html +0 -579
- django_cfg/apps/payments/templates/payments/payment_detail.html +0 -373
- django_cfg/apps/payments/templates/payments/payment_list.html +0 -354
- django_cfg/apps/payments/templates/payments/stats.html +0 -261
- django_cfg/apps/payments/templates/payments/test.html +0 -213
- django_cfg/apps/payments/templatetags/payments_tags.py +0 -315
- django_cfg/apps/payments/utils/__init__.py +0 -43
- django_cfg/apps/payments/utils/billing_utils.py +0 -342
- django_cfg/apps/payments/utils/config_utils.py +0 -239
- django_cfg/apps/payments/utils/middleware_utils.py +0 -228
- django_cfg/apps/payments/utils/validation_utils.py +0 -94
- django_cfg/apps/payments/views/__init__.py +0 -63
- django_cfg/apps/payments/views/api_key_views.py +0 -164
- django_cfg/apps/payments/views/balance_views.py +0 -75
- django_cfg/apps/payments/views/currency_views.py +0 -122
- django_cfg/apps/payments/views/payment_views.py +0 -149
- django_cfg/apps/payments/views/subscription_views.py +0 -135
- django_cfg/apps/payments/views/tariff_views.py +0 -131
- django_cfg/apps/payments/views/templates/__init__.py +0 -25
- django_cfg/apps/payments/views/templates/ajax.py +0 -451
- django_cfg/apps/payments/views/templates/base.py +0 -212
- django_cfg/apps/payments/views/templates/dashboard.py +0 -60
- django_cfg/apps/payments/views/templates/payment_detail.py +0 -102
- django_cfg/apps/payments/views/templates/payment_management.py +0 -158
- django_cfg/apps/payments/views/templates/qr_code.py +0 -174
- django_cfg/apps/payments/views/templates/stats.py +0 -244
- django_cfg/apps/payments/views/templates/utils.py +0 -181
- django_cfg/apps/payments/views/webhook_views.py +0 -266
- django_cfg/apps/payments/viewsets.py +0 -66
- django_cfg/core/integration.py +0 -160
- django_cfg/template_archive/.gitignore +0 -1
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/urls.py +0 -33
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.31.dist-info → django_cfg-1.3.1.dist-info}/entry_points.txt +0 -0
- {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)
|