django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +125 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,342 @@
|
|
1
|
+
"""
|
2
|
+
Payment System Notification Service
|
3
|
+
Uses existing django_telegram and django_email modules for admin notifications.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import Dict, Any, Optional
|
8
|
+
from django.utils import timezone
|
9
|
+
from django_cfg.modules.django_telegram import DjangoTelegram
|
10
|
+
from django_cfg.modules.django_email import DjangoEmailService
|
11
|
+
from django_cfg.core.config import get_current_config
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
config = get_current_config()
|
15
|
+
|
16
|
+
|
17
|
+
class PaymentNotifications:
|
18
|
+
"""
|
19
|
+
Payment system notifications using existing admin modules.
|
20
|
+
Follows the pattern from accounts/utils/notifications.py
|
21
|
+
"""
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def send_security_alert(error_info, context_info):
|
25
|
+
"""Send security alert notification using existing modules."""
|
26
|
+
try:
|
27
|
+
# Prepare notification data following the accounts pattern
|
28
|
+
notification_data = {
|
29
|
+
"error_code": error_info.error_code,
|
30
|
+
"category": error_info.category,
|
31
|
+
"severity": error_info.severity,
|
32
|
+
"message": error_info.message,
|
33
|
+
"timestamp": error_info.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
34
|
+
"provider": context_info.provider or "Unknown",
|
35
|
+
"user_id": context_info.user_id or "Unknown",
|
36
|
+
"operation": context_info.operation or "Unknown"
|
37
|
+
}
|
38
|
+
|
39
|
+
# Add request details if available
|
40
|
+
if context_info.request:
|
41
|
+
notification_data.update({
|
42
|
+
"ip_address": context_info.request.get("ip_address", "Unknown"),
|
43
|
+
"path": context_info.request.get("path", "Unknown"),
|
44
|
+
"method": context_info.request.get("method", "Unknown")
|
45
|
+
})
|
46
|
+
|
47
|
+
# Send telegram notification based on severity
|
48
|
+
if error_info.severity == "CRITICAL":
|
49
|
+
DjangoTelegram.send_error(
|
50
|
+
f"🚨 CRITICAL Payment Security Alert: {error_info.category}",
|
51
|
+
notification_data
|
52
|
+
)
|
53
|
+
elif error_info.severity == "HIGH":
|
54
|
+
DjangoTelegram.send_warning(
|
55
|
+
f"⚠️ HIGH Payment Security Alert: {error_info.category}",
|
56
|
+
notification_data
|
57
|
+
)
|
58
|
+
else:
|
59
|
+
DjangoTelegram.send_info(
|
60
|
+
f"ℹ️ Payment Security Alert: {error_info.category}",
|
61
|
+
notification_data
|
62
|
+
)
|
63
|
+
|
64
|
+
logger.info(f"Security alert notification sent: {error_info.error_code}")
|
65
|
+
|
66
|
+
except Exception as e:
|
67
|
+
logger.error(f"Failed to send security alert notification: {e}")
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def send_provider_error(error_info, context_info):
|
71
|
+
"""Send provider error notification."""
|
72
|
+
try:
|
73
|
+
provider = context_info.provider or "Unknown"
|
74
|
+
|
75
|
+
notification_data = {
|
76
|
+
"provider": provider,
|
77
|
+
"error_code": error_info.error_code,
|
78
|
+
"message": error_info.message,
|
79
|
+
"severity": error_info.severity,
|
80
|
+
"timestamp": error_info.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
81
|
+
"operation": context_info.operation or "Unknown",
|
82
|
+
"recoverable": error_info.recoverable
|
83
|
+
}
|
84
|
+
|
85
|
+
# Add provider-specific details if available
|
86
|
+
if hasattr(error_info.details, 'provider'):
|
87
|
+
notification_data["provider_details"] = error_info.details.provider
|
88
|
+
|
89
|
+
if error_info.severity in ["CRITICAL", "HIGH"]:
|
90
|
+
DjangoTelegram.send_error(
|
91
|
+
f"💳 Provider Error: {provider}",
|
92
|
+
notification_data
|
93
|
+
)
|
94
|
+
else:
|
95
|
+
DjangoTelegram.send_warning(
|
96
|
+
f"💳 Provider Issue: {provider}",
|
97
|
+
notification_data
|
98
|
+
)
|
99
|
+
|
100
|
+
logger.info(f"Provider error notification sent: {provider} - {error_info.error_code}")
|
101
|
+
|
102
|
+
except Exception as e:
|
103
|
+
logger.error(f"Failed to send provider error notification: {e}")
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def send_webhook_validation_failure(error_info, context_info):
|
107
|
+
"""Send webhook validation failure notification."""
|
108
|
+
try:
|
109
|
+
provider = context_info.provider or "Unknown"
|
110
|
+
|
111
|
+
notification_data = {
|
112
|
+
"provider": provider,
|
113
|
+
"error_code": error_info.error_code,
|
114
|
+
"validation_error": error_info.details.validation_error if hasattr(error_info.details, 'validation_error') else "Unknown",
|
115
|
+
"timestamp": error_info.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
116
|
+
"severity": "HIGH",
|
117
|
+
"requires_attention": True
|
118
|
+
}
|
119
|
+
|
120
|
+
# Add IP and request details for security analysis
|
121
|
+
if context_info.request:
|
122
|
+
notification_data.update({
|
123
|
+
"ip_address": context_info.request.get("ip_address", "Unknown"),
|
124
|
+
"user_agent": context_info.request.get("user_agent", "Unknown")
|
125
|
+
})
|
126
|
+
|
127
|
+
DjangoTelegram.send_warning(
|
128
|
+
f"🔒 Webhook Validation Failed: {provider}",
|
129
|
+
notification_data
|
130
|
+
)
|
131
|
+
|
132
|
+
logger.info(f"Webhook validation failure notification sent: {provider}")
|
133
|
+
|
134
|
+
except Exception as e:
|
135
|
+
logger.error(f"Failed to send webhook validation failure notification: {e}")
|
136
|
+
|
137
|
+
@staticmethod
|
138
|
+
def send_api_security_breach(error_info, context_info):
|
139
|
+
"""Send API security breach notification."""
|
140
|
+
try:
|
141
|
+
notification_data = {
|
142
|
+
"error_code": error_info.error_code,
|
143
|
+
"message": error_info.message,
|
144
|
+
"timestamp": error_info.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
145
|
+
"severity": "CRITICAL",
|
146
|
+
"middleware": context_info.middleware or "Unknown",
|
147
|
+
"operation": context_info.operation or "Unknown",
|
148
|
+
"requires_immediate_attention": True
|
149
|
+
}
|
150
|
+
|
151
|
+
# Add security-specific details
|
152
|
+
if hasattr(error_info.details, 'api_key_prefix'):
|
153
|
+
notification_data["api_key_prefix"] = error_info.details.api_key_prefix
|
154
|
+
|
155
|
+
if context_info.request:
|
156
|
+
notification_data.update({
|
157
|
+
"ip_address": context_info.request.get("ip_address", "Unknown"),
|
158
|
+
"path": context_info.request.get("path", "Unknown"),
|
159
|
+
"method": context_info.request.get("method", "Unknown"),
|
160
|
+
"user_agent": context_info.request.get("user_agent", "Unknown")
|
161
|
+
})
|
162
|
+
|
163
|
+
DjangoTelegram.send_error(
|
164
|
+
"🚨 API Security Breach Detected",
|
165
|
+
notification_data
|
166
|
+
)
|
167
|
+
|
168
|
+
logger.critical(f"API security breach notification sent: {error_info.error_code}")
|
169
|
+
|
170
|
+
except Exception as e:
|
171
|
+
logger.error(f"Failed to send API security breach notification: {e}")
|
172
|
+
|
173
|
+
@staticmethod
|
174
|
+
def send_payment_failure(error_info, context_info, payment_details=None):
|
175
|
+
"""Send payment failure notification."""
|
176
|
+
try:
|
177
|
+
notification_data = {
|
178
|
+
"error_code": error_info.error_code,
|
179
|
+
"category": error_info.category,
|
180
|
+
"message": error_info.message,
|
181
|
+
"timestamp": error_info.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
182
|
+
"provider": context_info.provider or "Unknown",
|
183
|
+
"user_id": context_info.user_id or "Unknown"
|
184
|
+
}
|
185
|
+
|
186
|
+
# Add payment-specific details if provided
|
187
|
+
if payment_details:
|
188
|
+
notification_data.update({
|
189
|
+
"payment_id": payment_details.get("payment_id"),
|
190
|
+
"amount_usd": payment_details.get("amount_usd"),
|
191
|
+
"currency": payment_details.get("currency"),
|
192
|
+
"order_id": payment_details.get("order_id")
|
193
|
+
})
|
194
|
+
|
195
|
+
if error_info.severity in ["CRITICAL", "HIGH"]:
|
196
|
+
DjangoTelegram.send_error(
|
197
|
+
f"💸 Payment Failure: {error_info.category}",
|
198
|
+
notification_data
|
199
|
+
)
|
200
|
+
else:
|
201
|
+
DjangoTelegram.send_warning(
|
202
|
+
f"💸 Payment Issue: {error_info.category}",
|
203
|
+
notification_data
|
204
|
+
)
|
205
|
+
|
206
|
+
logger.info(f"Payment failure notification sent: {error_info.error_code}")
|
207
|
+
|
208
|
+
except Exception as e:
|
209
|
+
logger.error(f"Failed to send payment failure notification: {e}")
|
210
|
+
|
211
|
+
@staticmethod
|
212
|
+
def send_system_error(error_info, context_info):
|
213
|
+
"""Send system error notification."""
|
214
|
+
try:
|
215
|
+
notification_data = {
|
216
|
+
"error_code": error_info.error_code,
|
217
|
+
"category": error_info.category,
|
218
|
+
"message": error_info.message,
|
219
|
+
"timestamp": error_info.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
|
220
|
+
"operation": context_info.operation or "Unknown",
|
221
|
+
"recoverable": error_info.recoverable
|
222
|
+
}
|
223
|
+
|
224
|
+
# Add system context
|
225
|
+
if context_info.system:
|
226
|
+
notification_data.update({
|
227
|
+
"environment": context_info.system.get("environment", "Unknown"),
|
228
|
+
"debug_mode": context_info.system.get("debug", False)
|
229
|
+
})
|
230
|
+
|
231
|
+
# Add exception details if available
|
232
|
+
if hasattr(error_info.details, 'exception_type'):
|
233
|
+
notification_data.update({
|
234
|
+
"exception_type": error_info.details.exception_type,
|
235
|
+
"exception_module": error_info.details.exception_module
|
236
|
+
})
|
237
|
+
|
238
|
+
if error_info.severity == "CRITICAL":
|
239
|
+
DjangoTelegram.send_error(
|
240
|
+
f"🔧 CRITICAL System Error",
|
241
|
+
notification_data
|
242
|
+
)
|
243
|
+
elif error_info.severity == "HIGH":
|
244
|
+
DjangoTelegram.send_warning(
|
245
|
+
f"🔧 System Error",
|
246
|
+
notification_data
|
247
|
+
)
|
248
|
+
else:
|
249
|
+
DjangoTelegram.send_info(
|
250
|
+
f"🔧 System Issue",
|
251
|
+
notification_data
|
252
|
+
)
|
253
|
+
|
254
|
+
logger.info(f"System error notification sent: {error_info.error_code}")
|
255
|
+
|
256
|
+
except Exception as e:
|
257
|
+
logger.error(f"Failed to send system error notification: {e}")
|
258
|
+
|
259
|
+
@staticmethod
|
260
|
+
def send_high_error_rate_alert(category, error_count, threshold):
|
261
|
+
"""Send high error rate alert notification."""
|
262
|
+
try:
|
263
|
+
notification_data = {
|
264
|
+
"category": category,
|
265
|
+
"error_count": error_count,
|
266
|
+
"threshold": threshold,
|
267
|
+
"timestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S UTC"),
|
268
|
+
"alert_type": "HIGH_ERROR_RATE",
|
269
|
+
"requires_immediate_attention": True
|
270
|
+
}
|
271
|
+
|
272
|
+
DjangoTelegram.send_error(
|
273
|
+
f"🚨 HIGH ERROR RATE DETECTED: {category}",
|
274
|
+
notification_data
|
275
|
+
)
|
276
|
+
|
277
|
+
logger.critical(f"High error rate alert sent: {category} - {error_count} errors")
|
278
|
+
|
279
|
+
except Exception as e:
|
280
|
+
logger.error(f"Failed to send high error rate alert: {e}")
|
281
|
+
|
282
|
+
@staticmethod
|
283
|
+
def send_attack_pattern_alert(ip_address, error_count, error_details):
|
284
|
+
"""Send security attack pattern alert."""
|
285
|
+
try:
|
286
|
+
notification_data = {
|
287
|
+
"ip_address": ip_address,
|
288
|
+
"error_count": error_count,
|
289
|
+
"timestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S UTC"),
|
290
|
+
"alert_type": "SECURITY_ATTACK_PATTERN",
|
291
|
+
"error_details": error_details,
|
292
|
+
"requires_immediate_action": True,
|
293
|
+
"recommended_action": "Consider IP blocking"
|
294
|
+
}
|
295
|
+
|
296
|
+
DjangoTelegram.send_error(
|
297
|
+
f"🚨 SECURITY ATTACK PATTERN DETECTED",
|
298
|
+
notification_data
|
299
|
+
)
|
300
|
+
|
301
|
+
logger.critical(f"Attack pattern alert sent: IP {ip_address} - {error_count} security errors")
|
302
|
+
|
303
|
+
except Exception as e:
|
304
|
+
logger.error(f"Failed to send attack pattern alert: {e}")
|
305
|
+
|
306
|
+
@staticmethod
|
307
|
+
def send_recovery_notification(error_info, context_info, recovery_result):
|
308
|
+
"""Send recovery attempt notification."""
|
309
|
+
try:
|
310
|
+
notification_data = {
|
311
|
+
"error_code": error_info.error_code,
|
312
|
+
"category": error_info.category,
|
313
|
+
"recovery_attempted": recovery_result.attempted,
|
314
|
+
"recovery_success": recovery_result.success,
|
315
|
+
"recovery_actions": recovery_result.actions,
|
316
|
+
"recovery_message": recovery_result.message,
|
317
|
+
"timestamp": timezone.now().strftime("%Y-%m-%d %H:%M:%S UTC"),
|
318
|
+
"provider": context_info.provider or "Unknown"
|
319
|
+
}
|
320
|
+
|
321
|
+
if recovery_result.error:
|
322
|
+
notification_data["recovery_error"] = recovery_result.error
|
323
|
+
|
324
|
+
if recovery_result.success:
|
325
|
+
DjangoTelegram.send_success(
|
326
|
+
f"🔄 Recovery Successful: {error_info.category}",
|
327
|
+
notification_data
|
328
|
+
)
|
329
|
+
else:
|
330
|
+
DjangoTelegram.send_warning(
|
331
|
+
f"🔄 Recovery Attempted: {error_info.category}",
|
332
|
+
notification_data
|
333
|
+
)
|
334
|
+
|
335
|
+
logger.info(f"Recovery notification sent: {error_info.error_code}")
|
336
|
+
|
337
|
+
except Exception as e:
|
338
|
+
logger.error(f"Failed to send recovery notification: {e}")
|
339
|
+
|
340
|
+
|
341
|
+
# Singleton instance for import
|
342
|
+
payment_notifications = PaymentNotifications()
|