django-cfg 1.2.20__py3-none-any.whl → 1.2.22__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 (59) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/maintenance/admin/site_admin.py +47 -8
  3. django_cfg/apps/maintenance/migrations/0003_cloudflaresite_include_subdomains_and_more.py +27 -0
  4. django_cfg/apps/maintenance/models/cloudflare_site.py +41 -0
  5. django_cfg/apps/maintenance/services/maintenance_service.py +121 -32
  6. django_cfg/apps/maintenance/services/site_sync_service.py +56 -11
  7. django_cfg/apps/newsletter/signals.py +9 -8
  8. django_cfg/apps/payments/__init__.py +8 -0
  9. django_cfg/apps/payments/apps.py +22 -0
  10. django_cfg/apps/payments/managers/__init__.py +22 -0
  11. django_cfg/apps/payments/managers/api_key_manager.py +35 -0
  12. django_cfg/apps/payments/managers/balance_manager.py +361 -0
  13. django_cfg/apps/payments/managers/currency_manager.py +32 -0
  14. django_cfg/apps/payments/managers/payment_manager.py +44 -0
  15. django_cfg/apps/payments/managers/subscription_manager.py +37 -0
  16. django_cfg/apps/payments/managers/tariff_manager.py +29 -0
  17. django_cfg/apps/payments/middleware/__init__.py +13 -0
  18. django_cfg/apps/payments/migrations/0001_initial.py +982 -0
  19. django_cfg/apps/payments/migrations/__init__.py +1 -0
  20. django_cfg/apps/payments/models/__init__.py +49 -0
  21. django_cfg/apps/payments/models/api_keys.py +96 -0
  22. django_cfg/apps/payments/models/balance.py +209 -0
  23. django_cfg/apps/payments/models/base.py +14 -0
  24. django_cfg/apps/payments/models/currencies.py +138 -0
  25. django_cfg/apps/payments/models/events.py +73 -0
  26. django_cfg/apps/payments/models/payments.py +301 -0
  27. django_cfg/apps/payments/models/subscriptions.py +270 -0
  28. django_cfg/apps/payments/models/tariffs.py +102 -0
  29. django_cfg/apps/payments/serializers/__init__.py +56 -0
  30. django_cfg/apps/payments/serializers/api_keys.py +51 -0
  31. django_cfg/apps/payments/serializers/balance.py +59 -0
  32. django_cfg/apps/payments/serializers/currencies.py +55 -0
  33. django_cfg/apps/payments/serializers/payments.py +62 -0
  34. django_cfg/apps/payments/serializers/subscriptions.py +71 -0
  35. django_cfg/apps/payments/serializers/tariffs.py +56 -0
  36. django_cfg/apps/payments/services/__init__.py +14 -0
  37. django_cfg/apps/payments/services/base.py +68 -0
  38. django_cfg/apps/payments/services/nowpayments.py +78 -0
  39. django_cfg/apps/payments/services/providers.py +77 -0
  40. django_cfg/apps/payments/services/redis_service.py +215 -0
  41. django_cfg/apps/payments/urls.py +78 -0
  42. django_cfg/apps/payments/views/__init__.py +62 -0
  43. django_cfg/apps/payments/views/api_key_views.py +164 -0
  44. django_cfg/apps/payments/views/balance_views.py +75 -0
  45. django_cfg/apps/payments/views/currency_views.py +111 -0
  46. django_cfg/apps/payments/views/payment_views.py +111 -0
  47. django_cfg/apps/payments/views/subscription_views.py +135 -0
  48. django_cfg/apps/payments/views/tariff_views.py +131 -0
  49. django_cfg/core/config.py +26 -1
  50. django_cfg/core/generation.py +2 -1
  51. django_cfg/management/commands/check_settings.py +54 -0
  52. django_cfg/models/revolution.py +14 -0
  53. django_cfg/modules/base.py +9 -0
  54. django_cfg/utils/smart_defaults.py +211 -85
  55. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/METADATA +1 -1
  56. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/RECORD +59 -17
  57. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/WHEEL +0 -0
  58. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/entry_points.txt +0 -0
  59. {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.2.20"
35
+ __version__ = "1.2.22"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -73,6 +73,7 @@ class CloudflareSiteAdmin(ModelAdmin):
73
73
  'status_display',
74
74
  'name',
75
75
  'domain',
76
+ 'subdomain_config_badge',
76
77
  'maintenance_badge',
77
78
  'active_badge',
78
79
  'last_maintenance_at',
@@ -102,6 +103,10 @@ class CloudflareSiteAdmin(ModelAdmin):
102
103
  ('Basic Information', {
103
104
  'fields': ['name', 'domain']
104
105
  }),
106
+ ('Subdomain Configuration', {
107
+ 'fields': ['include_subdomains', 'subdomain_list'],
108
+ 'description': 'Configure which subdomains should be affected by maintenance mode'
109
+ }),
105
110
  ('Cloudflare Configuration', {
106
111
  'fields': ['zone_id', 'account_id', 'api_key'],
107
112
  'classes': ['collapse']
@@ -141,6 +146,32 @@ class CloudflareSiteAdmin(ModelAdmin):
141
146
  # Display methods
142
147
 
143
148
  @display(description="Status")
149
+ @display(description="Subdomains", ordering='include_subdomains')
150
+ def subdomain_config_badge(self, obj: CloudflareSite) -> str:
151
+ """Display subdomain configuration with badge."""
152
+ config = obj.get_subdomain_display()
153
+
154
+ if obj.include_subdomains:
155
+ # All subdomains
156
+ badge_class = "badge-success"
157
+ icon = "🌐"
158
+ elif obj.subdomain_list.strip():
159
+ # Specific subdomains
160
+ badge_class = "badge-warning"
161
+ icon = "📋"
162
+ else:
163
+ # Root only
164
+ badge_class = "badge-secondary"
165
+ icon = "🏠"
166
+
167
+ return format_html(
168
+ '<span class="badge {}" title="{}">{} {}</span>',
169
+ badge_class,
170
+ config,
171
+ icon,
172
+ "All" if obj.include_subdomains else ("List" if obj.subdomain_list.strip() else "Root")
173
+ )
174
+
144
175
  def status_display(self, obj: CloudflareSite) -> str:
145
176
  """Display status with emoji."""
146
177
  if obj.maintenance_active:
@@ -299,6 +330,7 @@ class CloudflareSiteAdmin(ModelAdmin):
299
330
 
300
331
  @action(
301
332
  description="🔄 Sync with Cloudflare",
333
+ url_path="sync-cloudflare",
302
334
  icon="refresh",
303
335
  variant=ActionVariant.INFO
304
336
  )
@@ -310,15 +342,20 @@ class CloudflareSiteAdmin(ModelAdmin):
310
342
  messages.error(request, "Site not found.")
311
343
  return redirect(request.META.get('HTTP_REFERER', '/admin/'))
312
344
 
313
- # Use SiteSyncService for site synchronization
314
- from ..services import SiteSyncService
315
- sync_service = SiteSyncService(site.api_key)
316
- sync_service.sync_zones()
345
+ # Use convenience function for single site sync
346
+ from ..services import sync_site_from_cloudflare
347
+ log_entry = sync_site_from_cloudflare(site)
317
348
 
318
- messages.success(
319
- request,
320
- f"Site '{site.name}' has been synchronized with Cloudflare."
321
- )
349
+ if log_entry.status == log_entry.Status.SUCCESS:
350
+ messages.success(
351
+ request,
352
+ f"Site '{site.name}' has been synchronized with Cloudflare."
353
+ )
354
+ else:
355
+ messages.error(
356
+ request,
357
+ f"Failed to sync site '{site.name}': {log_entry.error_message}"
358
+ )
322
359
 
323
360
  except Exception as e:
324
361
  messages.error(request, f"Failed to sync site: {str(e)}")
@@ -327,6 +364,7 @@ class CloudflareSiteAdmin(ModelAdmin):
327
364
 
328
365
  @action(
329
366
  description="🔧 Enable Maintenance",
367
+ url_path="enable-maintenance",
330
368
  icon="build",
331
369
  variant=ActionVariant.WARNING
332
370
  )
@@ -350,6 +388,7 @@ class CloudflareSiteAdmin(ModelAdmin):
350
388
 
351
389
  @action(
352
390
  description="✅ Disable Maintenance",
391
+ url_path="disable-maintenance",
353
392
  icon="check_circle",
354
393
  variant=ActionVariant.SUCCESS
355
394
  )
@@ -0,0 +1,27 @@
1
+ # Generated by Django 5.2.6 on 2025-09-23 09:43
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("maintenance", "0002_cloudflaresite_maintenance_url"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="cloudflaresite",
14
+ name="include_subdomains",
15
+ field=models.BooleanField(
16
+ default=True, help_text="Apply maintenance rules to all subdomains (*.domain.com)"
17
+ ),
18
+ ),
19
+ migrations.AddField(
20
+ model_name="cloudflaresite",
21
+ name="subdomain_list",
22
+ field=models.TextField(
23
+ blank=True,
24
+ help_text="Comma-separated list of specific subdomains to include (e.g., api,www,app)",
25
+ ),
26
+ ),
27
+ ]
@@ -29,6 +29,16 @@ class CloudflareSite(models.Model):
29
29
  help_text="Domain name (e.g., vamcar.com)"
30
30
  )
31
31
 
32
+ # Subdomains configuration
33
+ include_subdomains = models.BooleanField(
34
+ default=True,
35
+ help_text="Apply maintenance rules to all subdomains (*.domain.com)"
36
+ )
37
+ subdomain_list = models.TextField(
38
+ blank=True,
39
+ help_text="Comma-separated list of specific subdomains to include (e.g., api,www,app)"
40
+ )
41
+
32
42
  # Cloudflare IDs (auto-discovered during sync)
33
43
  zone_id = models.CharField(
34
44
  max_length=32,
@@ -106,6 +116,37 @@ class CloudflareSite(models.Model):
106
116
  # Default maintenance page with site parameter
107
117
  return get_maintenance_url(self.domain)
108
118
 
119
+ def get_domain_patterns(self) -> list[str]:
120
+ """Get list of domain patterns for Page Rules based on subdomain configuration."""
121
+ patterns = []
122
+
123
+ if self.include_subdomains:
124
+ # Include all subdomains with wildcard
125
+ patterns.append(f"*{self.domain}/*")
126
+ # Also include root domain explicitly
127
+ patterns.append(f"{self.domain}/*")
128
+ else:
129
+ # Only root domain
130
+ patterns.append(f"{self.domain}/*")
131
+
132
+ # Add specific subdomains if specified
133
+ if self.subdomain_list.strip():
134
+ subdomains = [sub.strip() for sub in self.subdomain_list.split(',') if sub.strip()]
135
+ for subdomain in subdomains:
136
+ patterns.append(f"{subdomain}.{self.domain}/*")
137
+
138
+ return patterns
139
+
140
+ def get_subdomain_display(self) -> str:
141
+ """Get human-readable subdomain configuration."""
142
+ if self.include_subdomains:
143
+ return f"All subdomains (*.{self.domain})"
144
+ elif self.subdomain_list.strip():
145
+ subdomains = [sub.strip() for sub in self.subdomain_list.split(',') if sub.strip()]
146
+ return f"Specific: {', '.join(subdomains)}.{self.domain}"
147
+ else:
148
+ return f"Root domain only ({self.domain})"
149
+
109
150
  def clean(self) -> None:
110
151
  """Validate model data."""
111
152
  super().clean()
@@ -142,36 +142,87 @@ class MaintenanceService:
142
142
 
143
143
  @retry_on_failure(max_retries=3, base_delay=1.0)
144
144
  def _create_maintenance_page_rule(self) -> Dict[str, Any]:
145
- """Create Page Rule to redirect all traffic to maintenance page."""
145
+ """Create Page Rules to redirect all traffic to maintenance page (including subdomains)."""
146
146
  maintenance_url = self.site.get_maintenance_url()
147
- pattern = f"{self.site.domain}/*"
147
+ patterns = self.site.get_domain_patterns()
148
148
 
149
- logger.info(f"Creating page rule: {pattern} → {maintenance_url}")
149
+ logger.info(f"Creating page rules for {self.site.domain}: {patterns} → {maintenance_url}")
150
150
 
151
- response = self.client.page_rules.create(
151
+ # First, check if conflicting Page Rules already exist and delete them
152
+ try:
153
+ existing_rules = self.client.page_rules.list(zone_id=self.site.zone_id)
154
+
155
+ # Handle different API response formats
156
+ if hasattr(existing_rules, 'result'):
157
+ rules = existing_rules.result
158
+ else:
159
+ rules = existing_rules
160
+
161
+ # Look for conflicting rules with same patterns
162
+ for rule in rules:
163
+ if (hasattr(rule, 'targets') and rule.targets and
164
+ len(rule.targets) > 0 and
165
+ hasattr(rule.targets[0], 'constraint') and
166
+ hasattr(rule.targets[0].constraint, 'value')):
167
+
168
+ rule_pattern = rule.targets[0].constraint.value
169
+ if rule_pattern in patterns:
170
+ logger.info(f"Found conflicting page rule {rule.id} with pattern {rule_pattern}, deleting...")
171
+ self.client.page_rules.delete(
152
172
  zone_id=self.site.zone_id,
153
- targets=[{
154
- "target": "url",
155
- "constraint": {
156
- "operator": "matches",
157
- "value": pattern
158
- }
159
- }],
160
- actions=[{
161
- "id": "forwarding_url",
162
- "value": {
163
- "url": maintenance_url,
164
- "status_code": 302
165
- }
166
- }],
167
- status="active"
168
- )
169
- return response.model_dump()
173
+ pagerule_id=rule.id
174
+ )
175
+ logger.info(f"Deleted conflicting page rule {rule.id}")
176
+
177
+ except Exception as e:
178
+ logger.warning(f"Error checking existing page rules: {e}")
179
+ # Continue with creation anyway
180
+
181
+ # Create Page Rules for each pattern
182
+ created_rules = []
183
+ for pattern in patterns:
184
+ try:
185
+ logger.info(f"Creating page rule: {pattern} → {maintenance_url}")
186
+ response = self.client.page_rules.create(
187
+ zone_id=self.site.zone_id,
188
+ targets=[{
189
+ "target": "url",
190
+ "constraint": {
191
+ "operator": "matches",
192
+ "value": pattern
193
+ }
194
+ }],
195
+ actions=[{
196
+ "id": "forwarding_url",
197
+ "value": {
198
+ "url": maintenance_url,
199
+ "status_code": 302
200
+ }
201
+ }],
202
+ status="active"
203
+ )
204
+ created_rules.append({
205
+ "pattern": pattern,
206
+ "rule_id": response.id if hasattr(response, 'id') else 'unknown',
207
+ "response": response.model_dump()
208
+ })
209
+ logger.info(f"Created page rule for pattern {pattern}")
210
+
211
+ except Exception as e:
212
+ logger.error(f"Failed to create page rule for pattern {pattern}: {e}")
213
+ # Continue with other patterns
214
+
215
+ return {
216
+ "success": True,
217
+ "patterns": patterns,
218
+ "created_rules": created_rules,
219
+ "total_rules": len(created_rules)
220
+ }
170
221
 
171
222
  @retry_on_failure(max_retries=3, base_delay=1.0)
172
223
  def _delete_maintenance_page_rule(self) -> Dict[str, Any]:
173
- """Delete maintenance Page Rule with retry logic."""
174
- # Find the maintenance page rule
224
+ """Delete maintenance Page Rules with retry logic (including subdomains)."""
225
+ # Find the maintenance page rules
175
226
  page_rules_response = self.client.page_rules.list(zone_id=self.site.zone_id)
176
227
 
177
228
  # Handle different API response formats
@@ -180,37 +231,75 @@ class MaintenanceService:
180
231
  else:
181
232
  page_rules = page_rules_response
182
233
 
183
- maintenance_pattern = f"{self.site.domain}/*"
234
+ patterns = self.site.get_domain_patterns()
184
235
  maintenance_url = self.site.get_maintenance_url()
185
236
 
186
- logger.info(f"Looking for page rule to delete: pattern={maintenance_pattern}, url={maintenance_url}")
237
+ logger.info(f"Looking for page rules to delete: patterns={patterns}, url={maintenance_url}")
187
238
  logger.info(f"Found {len(page_rules)} page rules total")
188
239
 
240
+ deleted_rules = []
241
+
189
242
  for rule in page_rules:
190
243
  logger.info(f"Checking rule {rule.id}: targets={getattr(rule, 'targets', None)}, actions={getattr(rule, 'actions', None)}")
191
244
 
192
- # Simple check - look for forwarding_url action with maintenance URL
193
- if (hasattr(rule, 'actions') and rule.actions and
245
+ # Check if this rule matches our maintenance patterns
246
+ rule_matches = False
247
+
248
+ # Check by pattern
249
+ if (hasattr(rule, 'targets') and rule.targets and
250
+ len(rule.targets) > 0 and
251
+ hasattr(rule.targets[0], 'constraint') and
252
+ hasattr(rule.targets[0].constraint, 'value')):
253
+
254
+ rule_pattern = rule.targets[0].constraint.value
255
+ if rule_pattern in patterns:
256
+ rule_matches = True
257
+ logger.info(f"Rule {rule.id} matches pattern: {rule_pattern}")
258
+
259
+ # Also check by URL (fallback for older rules)
260
+ if not rule_matches and (hasattr(rule, 'actions') and rule.actions and
194
261
  len(rule.actions) > 0 and
195
262
  hasattr(rule.actions[0], 'id') and
196
263
  rule.actions[0].id == "forwarding_url"):
197
264
 
198
- # Check if URL contains maintenance.reforms.ai
199
265
  action_value = getattr(rule.actions[0], 'value', {})
200
266
  action_url = getattr(action_value, 'url', '')
201
267
 
202
268
  logger.info(f"Found forwarding rule with URL: {action_url}")
203
269
 
204
- if "maintenance.reforms.ai" in action_url:
270
+ if ("maintenance.reforms.ai" in action_url or
271
+ "djangocfg.com/maintenance" in action_url):
272
+ rule_matches = True
273
+ logger.info(f"Rule {rule.id} matches maintenance URL")
274
+
275
+ # Delete matching rule
276
+ if rule_matches:
277
+ try:
205
278
  logger.info(f"Deleting maintenance page rule: {rule.id}")
206
279
  response = self.client.page_rules.delete(
207
280
  zone_id=self.site.zone_id,
208
281
  pagerule_id=rule.id
209
282
  )
210
- return response.model_dump()
283
+ deleted_rules.append({
284
+ "rule_id": rule.id,
285
+ "pattern": getattr(rule.targets[0].constraint, 'value', 'unknown') if hasattr(rule, 'targets') and rule.targets else 'unknown',
286
+ "response": response.model_dump()
287
+ })
288
+ logger.info(f"Successfully deleted page rule: {rule.id}")
289
+
290
+ except Exception as e:
291
+ logger.error(f"Failed to delete page rule {rule.id}: {e}")
211
292
 
212
- logger.warning(f"No maintenance page rule found for {self.site.domain}")
213
- return {"success": True, "message": "No page rule to delete"}
293
+ if deleted_rules:
294
+ logger.info(f"Deleted {len(deleted_rules)} maintenance page rules for {self.site.domain}")
295
+ return {
296
+ "success": True,
297
+ "deleted_rules": deleted_rules,
298
+ "total_deleted": len(deleted_rules)
299
+ }
300
+ else:
301
+ logger.warning(f"No maintenance page rules found for {self.site.domain}")
302
+ return {"success": True, "message": "No page rules to delete"}
214
303
 
215
304
 
216
305
 
@@ -68,6 +68,7 @@ class SiteSyncService:
68
68
  logger.error(f"Failed to discover zones: {e}")
69
69
  raise CloudflareRetryError(f"Zone discovery failed: {e}")
70
70
 
71
+
71
72
  def sync_zones(self,
72
73
  force_update: bool = False,
73
74
  dry_run: bool = False) -> Dict[str, Any]:
@@ -163,9 +164,18 @@ class SiteSyncService:
163
164
  'changes': self._get_site_changes(site, zone_data)
164
165
  }
165
166
  else:
166
- # Update existing site
167
+ # Update existing site - preserve subdomain settings
167
168
  site.domain = domain
168
169
  site.account_id = zone_data.get('account_id', site.account_id)
170
+
171
+ # Ensure subdomain fields are not reset to None during sync
172
+ if site.include_subdomains is None:
173
+ site.include_subdomains = True
174
+ if site.subdomain_list is None:
175
+ site.subdomain_list = ''
176
+ if site.maintenance_url is None:
177
+ site.maintenance_url = ''
178
+
169
179
  site.save()
170
180
 
171
181
  # Log the sync
@@ -195,14 +205,18 @@ class SiteSyncService:
195
205
  'zone_data': zone_data
196
206
  }
197
207
  else:
198
- # Create new site
208
+ # Create new site with default subdomain settings
199
209
  site = CloudflareSite.objects.create(
200
210
  name=self._generate_site_name(domain),
201
211
  domain=domain,
202
212
  zone_id=zone_id,
203
213
  account_id=zone_data.get('account_id', ''),
204
214
  api_key=self.api_key,
205
- is_active=not zone_data.get('paused', False)
215
+ is_active=not zone_data.get('paused', False),
216
+ # Use default values from model
217
+ include_subdomains=True, # Default: include all subdomains
218
+ subdomain_list='', # Default: empty list
219
+ maintenance_url='' # Default: empty URL
206
220
  )
207
221
 
208
222
  # Log the creation
@@ -274,9 +288,18 @@ class SiteSyncService:
274
288
  'last_checked': timezone.now().isoformat()
275
289
  }
276
290
 
277
- # Update site status
291
+ # Update site status - preserve subdomain settings
278
292
  site.maintenance_active = maintenance_active
279
293
  site.is_active = not zone.paused
294
+
295
+ # Ensure subdomain fields are not reset to None during status update
296
+ if site.include_subdomains is None:
297
+ site.include_subdomains = True
298
+ if site.subdomain_list is None:
299
+ site.subdomain_list = ''
300
+ if site.maintenance_url is None:
301
+ site.maintenance_url = ''
302
+
280
303
  site.save()
281
304
 
282
305
  return status_info
@@ -373,14 +396,36 @@ def sync_site_from_cloudflare(site: CloudflareSite) -> MaintenanceLog:
373
396
  """
374
397
  try:
375
398
  sync_service = SiteSyncService(site.api_key)
376
- status_info = sync_service.check_site_status(site)
377
399
 
378
- return MaintenanceLog.log_success(
379
- site=site,
380
- action=MaintenanceLog.Action.SYNC,
381
- reason="Manual site sync",
382
- cloudflare_response=status_info
383
- )
400
+ # Perform full zone sync to update site data from Cloudflare
401
+ sync_result = sync_service.sync_zones(force_update=True)
402
+
403
+ # Find the specific site result
404
+ site_result = None
405
+ for site_info in sync_result.get('sites', []):
406
+ if site_info.get('domain') == site.domain:
407
+ site_result = site_info
408
+ break
409
+
410
+ response_data = {
411
+ 'sync_result': sync_result,
412
+ 'site_result': site_result
413
+ }
414
+
415
+ if sync_result.get('errors', 0) == 0:
416
+ return MaintenanceLog.log_success(
417
+ site=site,
418
+ action=MaintenanceLog.Action.SYNC,
419
+ reason="Manual site sync - force update from Cloudflare",
420
+ cloudflare_response=response_data
421
+ )
422
+ else:
423
+ return MaintenanceLog.log_failure(
424
+ site=site,
425
+ action=MaintenanceLog.Action.SYNC,
426
+ error_message=f"Sync completed with {sync_result['errors']} errors",
427
+ cloudflare_response=response_data
428
+ )
384
429
 
385
430
  except Exception as e:
386
431
  return MaintenanceLog.log_failure(
@@ -28,14 +28,15 @@ def newsletter_created(sender, instance, created, **kwargs):
28
28
  def subscription_created(sender, instance, created, **kwargs):
29
29
  """Handle newsletter subscription creation."""
30
30
  if created:
31
- logger.info(f"New subscription: {instance.email} to newsletter {instance.newsletter.title}")
32
- # Add logic for welcome email to new subscribers
33
- try:
34
- from .services.email_service import NewsletterEmailService
35
- email_service = NewsletterEmailService()
36
- email_service.send_subscription_welcome_email(instance)
37
- except Exception as e:
38
- logger.error(f"Failed to send welcome email to {instance.email}: {e}")
31
+ pass
32
+ # logger.info(f"New subscription: {instance.email} to newsletter {instance.newsletter.title}")
33
+ # # Add logic for welcome email to new subscribers
34
+ # try:
35
+ # from .services.email_service import NewsletterEmailService
36
+ # email_service = NewsletterEmailService()
37
+ # email_service.send_subscription_welcome_email(instance)
38
+ # except Exception as e:
39
+ # logger.error(f"Failed to send welcome email to {instance.email}: {e}")
39
40
 
40
41
 
41
42
  @receiver(pre_delete, sender=NewsletterSubscription)
@@ -0,0 +1,8 @@
1
+ """
2
+ Universal payments app for Django.
3
+
4
+ Provides reusable payment functionality with provider abstraction,
5
+ subscription management, and comprehensive audit trails.
6
+ """
7
+
8
+ default_app_config = 'django_cfg.apps.payments.apps.PaymentsConfig'
@@ -0,0 +1,22 @@
1
+ """
2
+ Django app configuration for universal payments.
3
+ """
4
+
5
+ from django.apps import AppConfig
6
+
7
+
8
+ class PaymentsConfig(AppConfig):
9
+ """Universal payments app configuration."""
10
+
11
+ default_auto_field = 'django.db.models.BigAutoField'
12
+ name = 'django_cfg.apps.payments'
13
+ label = 'django_cfg_payments'
14
+ verbose_name = 'Universal Payments'
15
+
16
+ def ready(self):
17
+ """Called when the app is ready."""
18
+ # Import signals if any
19
+ try:
20
+ from . import signals # noqa
21
+ except ImportError:
22
+ pass
@@ -0,0 +1,22 @@
1
+ """
2
+ Django model managers for universal payments.
3
+ """
4
+
5
+ from .payment_manager import UniversalPaymentManager
6
+ from .balance_manager import UserBalanceManager
7
+ from .subscription_manager import SubscriptionManager, EndpointGroupManager
8
+ from .tariff_manager import TariffManager, TariffEndpointGroupManager
9
+ from .api_key_manager import APIKeyManager
10
+ from .currency_manager import CurrencyManager, CurrencyNetworkManager
11
+
12
+ __all__ = [
13
+ 'UniversalPaymentManager',
14
+ 'UserBalanceManager',
15
+ 'SubscriptionManager',
16
+ 'EndpointGroupManager',
17
+ 'TariffManager',
18
+ 'TariffEndpointGroupManager',
19
+ 'APIKeyManager',
20
+ 'CurrencyManager',
21
+ 'CurrencyNetworkManager',
22
+ ]
@@ -0,0 +1,35 @@
1
+ """
2
+ API key managers.
3
+ """
4
+
5
+ from django.db import models
6
+ from django.utils import timezone
7
+
8
+
9
+ class APIKeyManager(models.Manager):
10
+ """Manager for APIKey model."""
11
+
12
+ def get_active_keys(self, user=None):
13
+ """Get active API keys."""
14
+ queryset = self.filter(is_active=True)
15
+ if user:
16
+ queryset = queryset.filter(user=user)
17
+ return queryset
18
+
19
+ def get_expired_keys(self):
20
+ """Get expired API keys."""
21
+ return self.filter(
22
+ expires_at__lte=timezone.now()
23
+ )
24
+
25
+ def get_valid_keys(self, user=None):
26
+ """Get valid (active and not expired) API keys."""
27
+ now = timezone.now()
28
+ queryset = self.filter(
29
+ is_active=True
30
+ ).filter(
31
+ models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now)
32
+ )
33
+ if user:
34
+ queryset = queryset.filter(user=user)
35
+ return queryset