django-cfg 1.3.1__py3-none-any.whl → 1.3.5__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/payments/admin_interface/old/payments/base.html +175 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
- django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
- django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
- django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
- django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
- django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
- django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
- django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
- django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
- django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
- django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
- django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
- django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
- django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
- django_cfg/apps/payments/admin_interface/views/base.py +114 -0
- django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
- django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
- django_cfg/apps/payments/config/helpers.py +2 -2
- django_cfg/apps/payments/management/commands/cleanup_expired_data.py +429 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +443 -0
- django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
- django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
- django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
- django_cfg/apps/payments/management/commands/test_providers.py +434 -0
- django_cfg/apps/payments/middleware/api_access.py +35 -34
- django_cfg/apps/payments/migrations/0001_initial.py +1 -1
- django_cfg/apps/payments/models/balance.py +5 -2
- django_cfg/apps/payments/models/managers/api_key_managers.py +6 -2
- django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
- django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
- django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
- django_cfg/apps/payments/models/subscriptions.py +0 -24
- django_cfg/apps/payments/services/cache/__init__.py +1 -1
- django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
- django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
- django_cfg/apps/payments/services/cache_service/interfaces.py +32 -0
- django_cfg/apps/payments/services/cache_service/keys.py +49 -0
- django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
- django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
- django_cfg/apps/payments/services/core/balance_service.py +13 -2
- django_cfg/apps/payments/services/core/payment_service.py +49 -22
- django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
- django_cfg/apps/payments/services/providers/registry.py +20 -0
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/balance_signals.py +8 -5
- django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
- django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
- django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
- django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
- django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
- django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
- django_cfg/apps/payments/urls.py +4 -0
- django_cfg/apps/payments/urls_admin.py +37 -18
- django_cfg/apps/payments/views/api/api_keys.py +14 -0
- django_cfg/apps/payments/views/api/base.py +1 -0
- django_cfg/apps/payments/views/api/currencies.py +2 -2
- django_cfg/apps/payments/views/api/payments.py +11 -5
- django_cfg/apps/payments/views/api/subscriptions.py +36 -31
- django_cfg/apps/payments/views/overview/__init__.py +40 -0
- django_cfg/apps/payments/views/overview/serializers.py +205 -0
- django_cfg/apps/payments/views/overview/services.py +439 -0
- django_cfg/apps/payments/views/overview/urls.py +27 -0
- django_cfg/apps/payments/views/overview/views.py +231 -0
- django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
- django_cfg/apps/payments/views/serializers/balances.py +5 -8
- django_cfg/apps/payments/views/serializers/currencies.py +2 -6
- django_cfg/apps/payments/views/serializers/payments.py +37 -32
- django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
- django_cfg/apps/urls.py +2 -1
- django_cfg/core/config.py +25 -15
- django_cfg/core/generation.py +12 -12
- django_cfg/core/integration/display/startup.py +1 -1
- django_cfg/core/validation.py +4 -4
- django_cfg/management/commands/show_config.py +2 -2
- django_cfg/management/commands/tree.py +1 -3
- django_cfg/middleware/__init__.py +2 -0
- django_cfg/middleware/static_nocache.py +55 -0
- django_cfg/models/payments.py +13 -15
- django_cfg/models/security.py +15 -0
- django_cfg/modules/django_ngrok.py +6 -0
- django_cfg/modules/django_unfold/dashboard.py +1 -3
- django_cfg/utils/smart_defaults.py +51 -5
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/RECORD +111 -69
- django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
- django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
- django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
- django_cfg/apps/payments/services/cache/cache_service.py +0 -235
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
- /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
- /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,357 @@
|
|
1
|
+
"""
|
2
|
+
Process Pending Payments Management Command for Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Automatically process pending payments, check statuses, and handle timeouts.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from datetime import timedelta
|
8
|
+
from typing import List, Dict, Any, Optional
|
9
|
+
from decimal import Decimal
|
10
|
+
|
11
|
+
from django.core.management.base import BaseCommand, CommandError
|
12
|
+
from django.utils import timezone
|
13
|
+
from django.db import transaction
|
14
|
+
from django.db.models import Q
|
15
|
+
|
16
|
+
from django_cfg.modules.django_logger import get_logger
|
17
|
+
from django_cfg.apps.payments.models import UniversalPayment
|
18
|
+
from django_cfg.apps.payments.services.providers.registry import get_provider_registry
|
19
|
+
from django_cfg.apps.payments.services.core.payment_service import PaymentService
|
20
|
+
from django_cfg.apps.payments.services.core.webhook_service import WebhookService
|
21
|
+
|
22
|
+
logger = get_logger("process_pending_payments")
|
23
|
+
|
24
|
+
|
25
|
+
class Command(BaseCommand):
|
26
|
+
"""
|
27
|
+
Process pending payments and update their statuses.
|
28
|
+
|
29
|
+
Features:
|
30
|
+
- Check payment statuses with providers
|
31
|
+
- Handle expired payments
|
32
|
+
- Process confirmations
|
33
|
+
- Update balances for completed payments
|
34
|
+
- Comprehensive logging and error handling
|
35
|
+
"""
|
36
|
+
|
37
|
+
help = 'Process pending payments and update their statuses'
|
38
|
+
|
39
|
+
def add_arguments(self, parser):
|
40
|
+
"""Add command line arguments."""
|
41
|
+
parser.add_argument(
|
42
|
+
'--provider',
|
43
|
+
type=str,
|
44
|
+
help='Process payments for specific provider only'
|
45
|
+
)
|
46
|
+
|
47
|
+
parser.add_argument(
|
48
|
+
'--payment-id',
|
49
|
+
type=str,
|
50
|
+
help='Process specific payment by ID'
|
51
|
+
)
|
52
|
+
|
53
|
+
parser.add_argument(
|
54
|
+
'--max-age-hours',
|
55
|
+
type=int,
|
56
|
+
default=72,
|
57
|
+
help='Maximum age in hours for pending payments (default: 72)'
|
58
|
+
)
|
59
|
+
|
60
|
+
parser.add_argument(
|
61
|
+
'--batch-size',
|
62
|
+
type=int,
|
63
|
+
default=50,
|
64
|
+
help='Number of payments to process in each batch (default: 50)'
|
65
|
+
)
|
66
|
+
|
67
|
+
parser.add_argument(
|
68
|
+
'--dry-run',
|
69
|
+
action='store_true',
|
70
|
+
help='Show what would be processed without making changes'
|
71
|
+
)
|
72
|
+
|
73
|
+
parser.add_argument(
|
74
|
+
'--force-expired',
|
75
|
+
action='store_true',
|
76
|
+
help='Force expire payments older than max-age-hours'
|
77
|
+
)
|
78
|
+
|
79
|
+
parser.add_argument(
|
80
|
+
'--verbose',
|
81
|
+
action='store_true',
|
82
|
+
help='Show detailed processing information'
|
83
|
+
)
|
84
|
+
|
85
|
+
def handle(self, *args, **options):
|
86
|
+
"""Execute the command."""
|
87
|
+
try:
|
88
|
+
self.options = options
|
89
|
+
self.dry_run = options['dry_run']
|
90
|
+
self.verbose = options['verbose']
|
91
|
+
|
92
|
+
self.show_header()
|
93
|
+
|
94
|
+
if options['payment_id']:
|
95
|
+
self.process_single_payment(options['payment_id'])
|
96
|
+
else:
|
97
|
+
self.process_pending_payments()
|
98
|
+
|
99
|
+
self.show_summary()
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
logger.error(f"Process pending payments command failed: {e}")
|
103
|
+
raise CommandError(f"Failed to process pending payments: {e}")
|
104
|
+
|
105
|
+
def show_header(self):
|
106
|
+
"""Display command header."""
|
107
|
+
mode = "DRY RUN" if self.dry_run else "LIVE MODE"
|
108
|
+
self.stdout.write(
|
109
|
+
self.style.SUCCESS("=" * 60)
|
110
|
+
)
|
111
|
+
self.stdout.write(
|
112
|
+
self.style.SUCCESS(f"⚡ PROCESS PENDING PAYMENTS - {mode}")
|
113
|
+
)
|
114
|
+
self.stdout.write(
|
115
|
+
self.style.SUCCESS("=" * 60)
|
116
|
+
)
|
117
|
+
self.stdout.write(f"Started: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
118
|
+
self.stdout.write("")
|
119
|
+
|
120
|
+
# Initialize counters
|
121
|
+
self.stats = {
|
122
|
+
'processed': 0,
|
123
|
+
'completed': 0,
|
124
|
+
'failed': 0,
|
125
|
+
'expired': 0,
|
126
|
+
'errors': 0,
|
127
|
+
'skipped': 0
|
128
|
+
}
|
129
|
+
|
130
|
+
def process_single_payment(self, payment_id: str):
|
131
|
+
"""Process a single payment by ID."""
|
132
|
+
try:
|
133
|
+
payment = UniversalPayment.objects.get(id=payment_id)
|
134
|
+
self.stdout.write(f"Processing single payment: {payment.id}")
|
135
|
+
|
136
|
+
result = self.process_payment(payment)
|
137
|
+
if result:
|
138
|
+
self.stdout.write(
|
139
|
+
self.style.SUCCESS(f"✅ Payment processed successfully")
|
140
|
+
)
|
141
|
+
else:
|
142
|
+
self.stdout.write(
|
143
|
+
self.style.ERROR(f"❌ Failed to process payment")
|
144
|
+
)
|
145
|
+
|
146
|
+
except UniversalPayment.DoesNotExist:
|
147
|
+
raise CommandError(f"Payment with ID {payment_id} not found")
|
148
|
+
|
149
|
+
def process_pending_payments(self):
|
150
|
+
"""Process all pending payments."""
|
151
|
+
# Build query filters
|
152
|
+
filters = Q(status__in=['pending', 'confirming', 'confirmed'])
|
153
|
+
|
154
|
+
# Provider filter
|
155
|
+
if self.options['provider']:
|
156
|
+
filters &= Q(provider=self.options['provider'])
|
157
|
+
|
158
|
+
# Age filter
|
159
|
+
max_age = timezone.now() - timedelta(hours=self.options['max_age_hours'])
|
160
|
+
if not self.options['force_expired']:
|
161
|
+
filters &= Q(created_at__gte=max_age)
|
162
|
+
|
163
|
+
# Get payments to process
|
164
|
+
payments = UniversalPayment.objects.filter(filters).select_related(
|
165
|
+
'user', 'currency', 'network'
|
166
|
+
).order_by('created_at')
|
167
|
+
|
168
|
+
total_payments = payments.count()
|
169
|
+
self.stdout.write(f"Found {total_payments} payments to process")
|
170
|
+
|
171
|
+
if total_payments == 0:
|
172
|
+
self.stdout.write(self.style.WARNING("No payments to process"))
|
173
|
+
return
|
174
|
+
|
175
|
+
# Process in batches
|
176
|
+
batch_size = self.options['batch_size']
|
177
|
+
processed = 0
|
178
|
+
|
179
|
+
for i in range(0, total_payments, batch_size):
|
180
|
+
batch = payments[i:i + batch_size]
|
181
|
+
self.stdout.write(f"\nProcessing batch {i//batch_size + 1} ({len(batch)} payments)...")
|
182
|
+
|
183
|
+
for payment in batch:
|
184
|
+
try:
|
185
|
+
self.process_payment(payment)
|
186
|
+
processed += 1
|
187
|
+
|
188
|
+
if self.verbose:
|
189
|
+
self.stdout.write(f" Processed: {payment.id} ({payment.status})")
|
190
|
+
|
191
|
+
except Exception as e:
|
192
|
+
self.stats['errors'] += 1
|
193
|
+
logger.error(f"Error processing payment {payment.id}: {e}")
|
194
|
+
if self.verbose:
|
195
|
+
self.stdout.write(
|
196
|
+
self.style.ERROR(f" Error: {payment.id} - {e}")
|
197
|
+
)
|
198
|
+
|
199
|
+
# Show progress
|
200
|
+
progress = (processed / total_payments) * 100
|
201
|
+
self.stdout.write(f"Progress: {processed}/{total_payments} ({progress:.1f}%)")
|
202
|
+
|
203
|
+
def process_payment(self, payment: UniversalPayment) -> bool:
|
204
|
+
"""
|
205
|
+
Process a single payment.
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
bool: True if payment was processed successfully
|
209
|
+
"""
|
210
|
+
try:
|
211
|
+
# Check if payment is too old and should be expired
|
212
|
+
max_age = timezone.now() - timedelta(hours=self.options['max_age_hours'])
|
213
|
+
if payment.created_at < max_age and self.options['force_expired']:
|
214
|
+
return self.expire_payment(payment)
|
215
|
+
|
216
|
+
# Check if payment has explicit expiration
|
217
|
+
if payment.expires_at and payment.expires_at < timezone.now():
|
218
|
+
return self.expire_payment(payment)
|
219
|
+
|
220
|
+
# Get provider and check status
|
221
|
+
provider_registry = get_provider_registry()
|
222
|
+
provider = provider_registry.get_provider(payment.provider)
|
223
|
+
|
224
|
+
if not provider:
|
225
|
+
logger.warning(f"Provider {payment.provider} not available for payment {payment.id}")
|
226
|
+
self.stats['skipped'] += 1
|
227
|
+
return False
|
228
|
+
|
229
|
+
# Check payment status with provider
|
230
|
+
if self.dry_run:
|
231
|
+
self.stdout.write(f" [DRY RUN] Would check status for payment {payment.id}")
|
232
|
+
self.stats['processed'] += 1
|
233
|
+
return True
|
234
|
+
|
235
|
+
# Get current status from provider
|
236
|
+
status_result = provider.get_payment_status(payment.provider_payment_id)
|
237
|
+
|
238
|
+
if not status_result.success:
|
239
|
+
logger.warning(f"Failed to get status for payment {payment.id}: {status_result.error}")
|
240
|
+
self.stats['errors'] += 1
|
241
|
+
return False
|
242
|
+
|
243
|
+
# Update payment based on provider status
|
244
|
+
old_status = payment.status
|
245
|
+
new_status = status_result.status
|
246
|
+
|
247
|
+
if old_status != new_status:
|
248
|
+
with transaction.atomic():
|
249
|
+
payment.status = new_status
|
250
|
+
|
251
|
+
# Update additional fields if provided
|
252
|
+
if hasattr(status_result, 'transaction_hash') and status_result.transaction_hash:
|
253
|
+
payment.transaction_hash = status_result.transaction_hash
|
254
|
+
|
255
|
+
if hasattr(status_result, 'confirmations') and status_result.confirmations is not None:
|
256
|
+
payment.confirmations_count = status_result.confirmations
|
257
|
+
|
258
|
+
if new_status == 'completed':
|
259
|
+
payment.completed_at = timezone.now()
|
260
|
+
self.stats['completed'] += 1
|
261
|
+
|
262
|
+
# Process balance update using service
|
263
|
+
payment_service = PaymentService()
|
264
|
+
payment_service.process_completed_payment(payment)
|
265
|
+
|
266
|
+
elif new_status in ['failed', 'expired', 'cancelled']:
|
267
|
+
self.stats['failed'] += 1
|
268
|
+
|
269
|
+
payment.save()
|
270
|
+
|
271
|
+
logger.info(f"Payment {payment.id} status updated: {old_status} -> {new_status}")
|
272
|
+
|
273
|
+
if self.verbose:
|
274
|
+
self.stdout.write(f" Updated: {payment.id} ({old_status} -> {new_status})")
|
275
|
+
|
276
|
+
self.stats['processed'] += 1
|
277
|
+
return True
|
278
|
+
|
279
|
+
except Exception as e:
|
280
|
+
logger.error(f"Error processing payment {payment.id}: {e}")
|
281
|
+
self.stats['errors'] += 1
|
282
|
+
return False
|
283
|
+
|
284
|
+
def expire_payment(self, payment: UniversalPayment) -> bool:
|
285
|
+
"""
|
286
|
+
Expire a payment that is too old.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
payment: Payment to expire
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
bool: True if payment was expired successfully
|
293
|
+
"""
|
294
|
+
try:
|
295
|
+
if self.dry_run:
|
296
|
+
self.stdout.write(f" [DRY RUN] Would expire payment {payment.id}")
|
297
|
+
return True
|
298
|
+
|
299
|
+
with transaction.atomic():
|
300
|
+
old_status = payment.status
|
301
|
+
payment.status = 'expired'
|
302
|
+
payment.save()
|
303
|
+
|
304
|
+
self.stats['expired'] += 1
|
305
|
+
logger.info(f"Payment {payment.id} expired (was {old_status})")
|
306
|
+
|
307
|
+
if self.verbose:
|
308
|
+
self.stdout.write(f" Expired: {payment.id} (was {old_status})")
|
309
|
+
|
310
|
+
return True
|
311
|
+
|
312
|
+
except Exception as e:
|
313
|
+
logger.error(f"Error expiring payment {payment.id}: {e}")
|
314
|
+
self.stats['errors'] += 1
|
315
|
+
return False
|
316
|
+
|
317
|
+
def show_summary(self):
|
318
|
+
"""Display processing summary."""
|
319
|
+
self.stdout.write("")
|
320
|
+
self.stdout.write(self.style.SUCCESS("📊 PROCESSING SUMMARY"))
|
321
|
+
self.stdout.write("-" * 40)
|
322
|
+
|
323
|
+
summary_items = [
|
324
|
+
("Processed", self.stats['processed'], 'SUCCESS'),
|
325
|
+
("Completed", self.stats['completed'], 'SUCCESS'),
|
326
|
+
("Failed", self.stats['failed'], 'WARNING'),
|
327
|
+
("Expired", self.stats['expired'], 'WARNING'),
|
328
|
+
("Errors", self.stats['errors'], 'ERROR'),
|
329
|
+
("Skipped", self.stats['skipped'], 'WARNING'),
|
330
|
+
]
|
331
|
+
|
332
|
+
for label, count, style in summary_items:
|
333
|
+
style_func = getattr(self.style, style)
|
334
|
+
self.stdout.write(f"{label:<12}: {style_func(count)}")
|
335
|
+
|
336
|
+
# Show completion time
|
337
|
+
self.stdout.write("")
|
338
|
+
self.stdout.write(f"Completed: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
339
|
+
|
340
|
+
# Show recommendations
|
341
|
+
if self.stats['errors'] > 0:
|
342
|
+
self.stdout.write("")
|
343
|
+
self.stdout.write(
|
344
|
+
self.style.WARNING("⚠️ Some payments had errors. Check logs for details.")
|
345
|
+
)
|
346
|
+
|
347
|
+
if self.stats['failed'] > 0:
|
348
|
+
self.stdout.write("")
|
349
|
+
self.stdout.write(
|
350
|
+
self.style.WARNING("⚠️ Some payments failed. Consider investigating failed payments.")
|
351
|
+
)
|
352
|
+
|
353
|
+
if self.dry_run:
|
354
|
+
self.stdout.write("")
|
355
|
+
self.stdout.write(
|
356
|
+
self.style.SUCCESS("✅ Dry run completed. Run without --dry-run to apply changes.")
|
357
|
+
)
|