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.
Files changed (115) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin_interface/old/payments/base.html +175 -0
  3. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
  4. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
  5. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
  6. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
  7. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
  8. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
  9. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
  10. django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
  11. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
  12. django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
  13. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
  14. django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
  23. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
  24. django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
  25. django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
  26. django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
  27. django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
  28. django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
  29. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
  30. django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
  31. django_cfg/apps/payments/admin_interface/views/base.py +114 -0
  32. django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
  33. django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
  34. django_cfg/apps/payments/config/helpers.py +2 -2
  35. django_cfg/apps/payments/management/commands/cleanup_expired_data.py +429 -0
  36. django_cfg/apps/payments/management/commands/currency_stats.py +443 -0
  37. django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
  38. django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
  39. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  40. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  41. django_cfg/apps/payments/middleware/api_access.py +35 -34
  42. django_cfg/apps/payments/migrations/0001_initial.py +1 -1
  43. django_cfg/apps/payments/models/balance.py +5 -2
  44. django_cfg/apps/payments/models/managers/api_key_managers.py +6 -2
  45. django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
  46. django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
  47. django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
  48. django_cfg/apps/payments/models/subscriptions.py +0 -24
  49. django_cfg/apps/payments/services/cache/__init__.py +1 -1
  50. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  51. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  52. django_cfg/apps/payments/services/cache_service/interfaces.py +32 -0
  53. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  54. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  55. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  56. django_cfg/apps/payments/services/core/balance_service.py +13 -2
  57. django_cfg/apps/payments/services/core/payment_service.py +49 -22
  58. django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
  59. django_cfg/apps/payments/services/providers/registry.py +20 -0
  60. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  61. django_cfg/apps/payments/signals/balance_signals.py +8 -5
  62. django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
  63. django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
  64. django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
  65. django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
  66. django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
  67. django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
  68. django_cfg/apps/payments/urls.py +4 -0
  69. django_cfg/apps/payments/urls_admin.py +37 -18
  70. django_cfg/apps/payments/views/api/api_keys.py +14 -0
  71. django_cfg/apps/payments/views/api/base.py +1 -0
  72. django_cfg/apps/payments/views/api/currencies.py +2 -2
  73. django_cfg/apps/payments/views/api/payments.py +11 -5
  74. django_cfg/apps/payments/views/api/subscriptions.py +36 -31
  75. django_cfg/apps/payments/views/overview/__init__.py +40 -0
  76. django_cfg/apps/payments/views/overview/serializers.py +205 -0
  77. django_cfg/apps/payments/views/overview/services.py +439 -0
  78. django_cfg/apps/payments/views/overview/urls.py +27 -0
  79. django_cfg/apps/payments/views/overview/views.py +231 -0
  80. django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
  81. django_cfg/apps/payments/views/serializers/balances.py +5 -8
  82. django_cfg/apps/payments/views/serializers/currencies.py +2 -6
  83. django_cfg/apps/payments/views/serializers/payments.py +37 -32
  84. django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
  85. django_cfg/apps/urls.py +2 -1
  86. django_cfg/core/config.py +25 -15
  87. django_cfg/core/generation.py +12 -12
  88. django_cfg/core/integration/display/startup.py +1 -1
  89. django_cfg/core/validation.py +4 -4
  90. django_cfg/management/commands/show_config.py +2 -2
  91. django_cfg/management/commands/tree.py +1 -3
  92. django_cfg/middleware/__init__.py +2 -0
  93. django_cfg/middleware/static_nocache.py +55 -0
  94. django_cfg/models/payments.py +13 -15
  95. django_cfg/models/security.py +15 -0
  96. django_cfg/modules/django_ngrok.py +6 -0
  97. django_cfg/modules/django_unfold/dashboard.py +1 -3
  98. django_cfg/utils/smart_defaults.py +51 -5
  99. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
  100. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/RECORD +111 -69
  101. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
  102. django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
  103. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
  104. django_cfg/apps/payments/services/cache/cache_service.py +0 -235
  105. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
  106. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
  107. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
  108. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
  109. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
  110. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
  111. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
  112. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
  113. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
  114. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
  115. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,429 @@
1
+ """
2
+ Cleanup Expired Data Management Command for Universal Payment System v2.0.
3
+
4
+ Clean up expired payments, sessions, and other temporary data.
5
+ """
6
+
7
+ from datetime import timedelta
8
+ from typing import List, Dict, Any, Optional
9
+
10
+ from django.core.management.base import BaseCommand, CommandError
11
+ from django.utils import timezone
12
+ from django.db import transaction
13
+ from django.db.models import Q, Count
14
+ from django.core.cache import cache
15
+
16
+ from django_cfg.modules.django_logger import get_logger
17
+ from django_cfg.apps.payments.models import UniversalPayment, APIKey, Transaction
18
+ from django_cfg.apps.payments.services.cache_service import get_cache_service
19
+
20
+ logger = get_logger("cleanup_expired_data")
21
+
22
+
23
+ class Command(BaseCommand):
24
+ """
25
+ Clean up expired data from the payment system.
26
+
27
+ Features:
28
+ - Remove expired payments
29
+ - Clean up expired API keys
30
+ - Remove old transaction logs
31
+ - Clear stale cache entries
32
+ - Comprehensive logging and statistics
33
+ """
34
+
35
+ help = 'Clean up expired data from the payment system'
36
+
37
+ def add_arguments(self, parser):
38
+ """Add command line arguments."""
39
+ parser.add_argument(
40
+ '--payments-age-days',
41
+ type=int,
42
+ default=30,
43
+ help='Remove failed/expired payments older than N days (default: 30)'
44
+ )
45
+
46
+ parser.add_argument(
47
+ '--transactions-age-days',
48
+ type=int,
49
+ default=90,
50
+ help='Remove old transaction logs older than N days (default: 90)'
51
+ )
52
+
53
+ parser.add_argument(
54
+ '--api-keys',
55
+ action='store_true',
56
+ help='Clean up expired API keys'
57
+ )
58
+
59
+ parser.add_argument(
60
+ '--cache',
61
+ action='store_true',
62
+ help='Clear stale cache entries'
63
+ )
64
+
65
+ parser.add_argument(
66
+ '--all',
67
+ action='store_true',
68
+ help='Clean up all types of expired data'
69
+ )
70
+
71
+ parser.add_argument(
72
+ '--payments-only',
73
+ action='store_true',
74
+ help='Clean up only expired payments'
75
+ )
76
+
77
+ parser.add_argument(
78
+ '--dry-run',
79
+ action='store_true',
80
+ help='Show what would be cleaned without making changes'
81
+ )
82
+
83
+ parser.add_argument(
84
+ '--batch-size',
85
+ type=int,
86
+ default=1000,
87
+ help='Number of records to process in each batch (default: 1000)'
88
+ )
89
+
90
+ parser.add_argument(
91
+ '--verbose',
92
+ action='store_true',
93
+ help='Show detailed cleanup information'
94
+ )
95
+
96
+ def handle(self, *args, **options):
97
+ """Execute the command."""
98
+ try:
99
+ self.options = options
100
+ self.dry_run = options['dry_run']
101
+ self.verbose = options['verbose']
102
+
103
+ self.show_header()
104
+
105
+ # Initialize statistics
106
+ self.stats = {
107
+ 'payments_removed': 0,
108
+ 'transactions_removed': 0,
109
+ 'api_keys_removed': 0,
110
+ 'cache_entries_cleared': 0,
111
+ 'errors': 0
112
+ }
113
+
114
+ # Determine what to clean
115
+ clean_all = options['all']
116
+ payments_only = options['payments_only']
117
+
118
+ if payments_only:
119
+ # Only clean payments
120
+ self.cleanup_expired_payments()
121
+ elif clean_all or not any([options['api_keys'], options['cache']]):
122
+ # Default: clean payments and transactions
123
+ self.cleanup_expired_payments()
124
+ self.cleanup_old_transactions()
125
+
126
+ if not payments_only and (clean_all or options['api_keys']):
127
+ self.cleanup_expired_api_keys()
128
+
129
+ if not payments_only and (clean_all or options['cache']):
130
+ self.cleanup_stale_cache()
131
+
132
+ self.show_summary()
133
+
134
+ except Exception as e:
135
+ logger.error(f"Cleanup expired data command failed: {e}")
136
+ raise CommandError(f"Failed to cleanup expired data: {e}")
137
+
138
+ def show_header(self):
139
+ """Display command header."""
140
+ mode = "DRY RUN" if self.dry_run else "LIVE MODE"
141
+ self.stdout.write(
142
+ self.style.SUCCESS("=" * 60)
143
+ )
144
+ self.stdout.write(
145
+ self.style.SUCCESS(f"🧹 CLEANUP EXPIRED DATA - {mode}")
146
+ )
147
+ self.stdout.write(
148
+ self.style.SUCCESS("=" * 60)
149
+ )
150
+ self.stdout.write(f"Started: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
151
+ self.stdout.write("")
152
+
153
+ def cleanup_expired_payments(self):
154
+ """Clean up expired and failed payments."""
155
+ self.stdout.write(self.style.SUCCESS("🗑️ CLEANING UP EXPIRED PAYMENTS"))
156
+ self.stdout.write("-" * 40)
157
+
158
+ # Calculate cutoff date
159
+ cutoff_date = timezone.now() - timedelta(days=self.options['payments_age_days'])
160
+
161
+ # Find payments to remove
162
+ expired_payments = UniversalPayment.objects.filter(
163
+ Q(status__in=['failed', 'expired', 'cancelled']) &
164
+ Q(created_at__lt=cutoff_date)
165
+ )
166
+
167
+ total_count = expired_payments.count()
168
+ self.stdout.write(f"Found {total_count} expired payments to remove")
169
+
170
+ if total_count == 0:
171
+ self.stdout.write(self.style.WARNING("No expired payments to clean up"))
172
+ return
173
+
174
+ if self.dry_run:
175
+ self.stdout.write(f"[DRY RUN] Would remove {total_count} expired payments")
176
+ self.stats['payments_removed'] = total_count
177
+ return
178
+
179
+ # Remove in batches
180
+ batch_size = self.options['batch_size']
181
+ removed_count = 0
182
+
183
+ try:
184
+ while True:
185
+ # Get batch of payments to delete
186
+ batch_ids = list(
187
+ expired_payments.values_list('id', flat=True)[:batch_size]
188
+ )
189
+
190
+ if not batch_ids:
191
+ break
192
+
193
+ with transaction.atomic():
194
+ # Delete the batch
195
+ deleted_count = UniversalPayment.objects.filter(
196
+ id__in=batch_ids
197
+ ).delete()[0]
198
+
199
+ removed_count += deleted_count
200
+
201
+ if self.verbose:
202
+ self.stdout.write(f" Removed batch: {deleted_count} payments")
203
+
204
+ # Update progress
205
+ progress = (removed_count / total_count) * 100
206
+ self.stdout.write(f"Progress: {removed_count}/{total_count} ({progress:.1f}%)")
207
+
208
+ self.stats['payments_removed'] = removed_count
209
+ logger.info(f"Removed {removed_count} expired payments")
210
+
211
+ except Exception as e:
212
+ logger.error(f"Error cleaning up payments: {e}")
213
+ self.stats['errors'] += 1
214
+ self.stdout.write(self.style.ERROR(f"Error: {e}"))
215
+
216
+ self.stdout.write("")
217
+
218
+ def cleanup_old_transactions(self):
219
+ """Clean up old transaction logs."""
220
+ self.stdout.write(self.style.SUCCESS("📋 CLEANING UP OLD TRANSACTIONS"))
221
+ self.stdout.write("-" * 40)
222
+
223
+ # Calculate cutoff date
224
+ cutoff_date = timezone.now() - timedelta(days=self.options['transactions_age_days'])
225
+
226
+ # Find transactions to remove (keep important ones)
227
+ old_transactions = Transaction.objects.filter(
228
+ created_at__lt=cutoff_date
229
+ ).exclude(
230
+ # Keep transactions with payment references (important transactions)
231
+ payment_id__isnull=False
232
+ )
233
+
234
+ total_count = old_transactions.count()
235
+ self.stdout.write(f"Found {total_count} old transactions to remove")
236
+
237
+ if total_count == 0:
238
+ self.stdout.write(self.style.WARNING("No old transactions to clean up"))
239
+ return
240
+
241
+ if self.dry_run:
242
+ self.stdout.write(f"[DRY RUN] Would remove {total_count} old transactions")
243
+ self.stats['transactions_removed'] = total_count
244
+ return
245
+
246
+ # Remove in batches
247
+ batch_size = self.options['batch_size']
248
+ removed_count = 0
249
+
250
+ try:
251
+ while True:
252
+ # Get batch of transactions to delete
253
+ batch_ids = list(
254
+ old_transactions.values_list('id', flat=True)[:batch_size]
255
+ )
256
+
257
+ if not batch_ids:
258
+ break
259
+
260
+ with transaction.atomic():
261
+ # Delete the batch
262
+ deleted_count = Transaction.objects.filter(
263
+ id__in=batch_ids
264
+ ).delete()[0]
265
+
266
+ removed_count += deleted_count
267
+
268
+ if self.verbose:
269
+ self.stdout.write(f" Removed batch: {deleted_count} transactions")
270
+
271
+ # Update progress
272
+ progress = (removed_count / total_count) * 100
273
+ self.stdout.write(f"Progress: {removed_count}/{total_count} ({progress:.1f}%)")
274
+
275
+ self.stats['transactions_removed'] = removed_count
276
+ logger.info(f"Removed {removed_count} old transactions")
277
+
278
+ except Exception as e:
279
+ logger.error(f"Error cleaning up transactions: {e}")
280
+ self.stats['errors'] += 1
281
+ self.stdout.write(self.style.ERROR(f"Error: {e}"))
282
+
283
+ self.stdout.write("")
284
+
285
+ def cleanup_expired_api_keys(self):
286
+ """Clean up expired API keys."""
287
+ self.stdout.write(self.style.SUCCESS("🔑 CLEANING UP EXPIRED API KEYS"))
288
+ self.stdout.write("-" * 40)
289
+
290
+ # Find expired API keys
291
+ now = timezone.now()
292
+ expired_keys = APIKey.objects.filter(
293
+ Q(expires_at__lt=now) | Q(is_active=False)
294
+ ).filter(
295
+ # Only remove keys that haven't been used recently
296
+ last_used_at__lt=now - timedelta(days=7)
297
+ )
298
+
299
+ total_count = expired_keys.count()
300
+ self.stdout.write(f"Found {total_count} expired API keys to remove")
301
+
302
+ if total_count == 0:
303
+ self.stdout.write(self.style.WARNING("No expired API keys to clean up"))
304
+ return
305
+
306
+ if self.dry_run:
307
+ self.stdout.write(f"[DRY RUN] Would remove {total_count} expired API keys")
308
+ self.stats['api_keys_removed'] = total_count
309
+ return
310
+
311
+ try:
312
+ # Remove expired keys
313
+ removed_count = expired_keys.delete()[0]
314
+ self.stats['api_keys_removed'] = removed_count
315
+
316
+ self.stdout.write(f"Removed {removed_count} expired API keys")
317
+ logger.info(f"Removed {removed_count} expired API keys")
318
+
319
+ except Exception as e:
320
+ logger.error(f"Error cleaning up API keys: {e}")
321
+ self.stats['errors'] += 1
322
+ self.stdout.write(self.style.ERROR(f"Error: {e}"))
323
+
324
+ self.stdout.write("")
325
+
326
+ def cleanup_stale_cache(self):
327
+ """Clean up stale cache entries."""
328
+ self.stdout.write(self.style.SUCCESS("💾 CLEANING UP STALE CACHE"))
329
+ self.stdout.write("-" * 40)
330
+
331
+ if self.dry_run:
332
+ self.stdout.write("[DRY RUN] Would clear stale cache entries")
333
+ self.stats['cache_entries_cleared'] = 100 # Estimate
334
+ return
335
+
336
+ try:
337
+ # Get cache service
338
+ cache_service = get_cache_service()
339
+
340
+ # Clear payment-related caches
341
+ cache_patterns = [
342
+ 'payment:*',
343
+ 'balance:*',
344
+ 'api_key:*',
345
+ 'currency:*',
346
+ 'provider:*',
347
+ 'rate_limit:*'
348
+ ]
349
+
350
+ cleared_count = 0
351
+
352
+ for pattern in cache_patterns:
353
+ try:
354
+ # Clear cache entries matching pattern
355
+ if hasattr(cache_service, 'clear_pattern'):
356
+ count = cache_service.clear_pattern(pattern)
357
+ cleared_count += count
358
+ if self.verbose:
359
+ self.stdout.write(f" Cleared {count} entries for pattern: {pattern}")
360
+ except Exception as e:
361
+ logger.warning(f"Failed to clear cache pattern {pattern}: {e}")
362
+
363
+ # Fallback: clear all cache if pattern clearing not available
364
+ if cleared_count == 0:
365
+ cache.clear()
366
+ cleared_count = 1 # At least one operation
367
+ self.stdout.write("Cleared all cache entries")
368
+
369
+ self.stats['cache_entries_cleared'] = cleared_count
370
+ logger.info(f"Cleared {cleared_count} cache entries")
371
+
372
+ except Exception as e:
373
+ logger.error(f"Error cleaning up cache: {e}")
374
+ self.stats['errors'] += 1
375
+ self.stdout.write(self.style.ERROR(f"Error: {e}"))
376
+
377
+ self.stdout.write("")
378
+
379
+ def show_summary(self):
380
+ """Display cleanup summary."""
381
+ self.stdout.write(self.style.SUCCESS("📊 CLEANUP SUMMARY"))
382
+ self.stdout.write("-" * 40)
383
+
384
+ summary_items = [
385
+ ("Payments Removed", self.stats['payments_removed']),
386
+ ("Transactions Removed", self.stats['transactions_removed']),
387
+ ("API Keys Removed", self.stats['api_keys_removed']),
388
+ ("Cache Entries Cleared", self.stats['cache_entries_cleared']),
389
+ ("Errors", self.stats['errors']),
390
+ ]
391
+
392
+ for label, count in summary_items:
393
+ if count > 0:
394
+ style = self.style.SUCCESS if label != "Errors" else self.style.ERROR
395
+ self.stdout.write(f"{label:<22}: {style(count)}")
396
+
397
+ # Calculate total items processed
398
+ total_processed = (
399
+ self.stats['payments_removed'] +
400
+ self.stats['transactions_removed'] +
401
+ self.stats['api_keys_removed']
402
+ )
403
+
404
+ if total_processed > 0:
405
+ self.stdout.write("")
406
+ self.stdout.write(f"Total items processed: {self.style.SUCCESS(total_processed)}")
407
+
408
+ # Show completion time
409
+ self.stdout.write("")
410
+ self.stdout.write(f"Completed: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
411
+
412
+ # Show recommendations
413
+ if self.stats['errors'] > 0:
414
+ self.stdout.write("")
415
+ self.stdout.write(
416
+ self.style.WARNING("⚠️ Some cleanup operations had errors. Check logs for details.")
417
+ )
418
+
419
+ if total_processed == 0 and self.stats['errors'] == 0:
420
+ self.stdout.write("")
421
+ self.stdout.write(
422
+ self.style.SUCCESS("✅ No expired data found. System is clean!")
423
+ )
424
+
425
+ if self.dry_run and total_processed > 0:
426
+ self.stdout.write("")
427
+ self.stdout.write(
428
+ self.style.SUCCESS("✅ Dry run completed. Run without --dry-run to apply changes.")
429
+ )