fix error in gitignore

This commit is contained in:
2026-09-24 12:32:59 +03:30
commit 86c117ee2a
165 changed files with 5963 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# notifications app
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.notifications"
@@ -0,0 +1,64 @@
# Generated by Django 5.2.17 on 2026-09-15 09:29
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='NotificationTemplate',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('event', models.CharField(db_index=True, max_length=120)),
('channel', models.CharField(choices=[('sms', 'SMS'), ('email', 'Email'), ('push', 'Push'), ('inapp', 'In-app')], default='sms', max_length=12)),
('subject', models.CharField(blank=True, max_length=255)),
('body', models.TextField(help_text='Supports {{variable}} placeholders.')),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='NotificationRule',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('event', models.CharField(db_index=True, max_length=120)),
('recipients', models.JSONField(blank=True, default=list)),
('enabled', models.BooleanField(default=True)),
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='notifications.notificationtemplate')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='NotificationLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('recipient', models.CharField(blank=True, max_length=200)),
('channel', models.CharField(max_length=12)),
('status', models.CharField(default='pending', max_length=20)),
('error', models.TextField(blank=True)),
('sent_at', models.DateTimeField(blank=True, null=True)),
('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='notifications.notificationtemplate')),
],
options={
'abstract': False,
},
),
]
+36
View File
@@ -0,0 +1,36 @@
from django.db import models
from apps.core.models import TimeStampedModel, UUIDModel
class NotificationTemplate(UUIDModel, TimeStampedModel):
class Channel(models.TextChoices):
SMS = "sms", "SMS"
EMAIL = "email", "Email"
PUSH = "push", "Push"
INAPP = "inapp", "In-app"
event = models.CharField(max_length=120, db_index=True)
channel = models.CharField(max_length=12, choices=Channel.choices, default=Channel.SMS)
subject = models.CharField(max_length=255, blank=True)
body = models.TextField(help_text="Supports {{variable}} placeholders.")
enabled = models.BooleanField(default=True)
def __str__(self) -> str:
return f"{self.event} ({self.channel})"
class NotificationRule(UUIDModel, TimeStampedModel):
event = models.CharField(max_length=120, db_index=True)
template = models.ForeignKey(NotificationTemplate, on_delete=models.CASCADE, related_name="rules")
recipients = models.JSONField(default=list, blank=True)
enabled = models.BooleanField(default=True)
class NotificationLog(UUIDModel, TimeStampedModel):
template = models.ForeignKey(NotificationTemplate, null=True, blank=True, on_delete=models.SET_NULL)
recipient = models.CharField(max_length=200, blank=True)
channel = models.CharField(max_length=12)
status = models.CharField(max_length=20, default="pending")
error = models.TextField(blank=True)
sent_at = models.DateTimeField(null=True, blank=True)
+61
View File
@@ -0,0 +1,61 @@
"""Pluggable SMS / notification providers.
Providers are selected at runtime from the dynamic settings store, so an SMS
gateway can be configured from the admin panel without touching code. Business
logic only ever talks to the provider interface.
"""
import json
import re
import urllib.request
from typing import Protocol
from apps.core.settings_service import SettingsService
class SmsProviderInterface(Protocol):
def send(self, recipient: str, message: str) -> str:
"""Return the provider message id."""
...
class MockSmsProvider:
def send(self, recipient: str, message: str) -> str:
return f"MOCK-{recipient}-{abs(hash(message)) % 10_000_000}"
class HttpSmsProvider:
"""Generic HTTP gateway: POSTs a JSON payload to a configurable endpoint."""
def __init__(self, endpoint: str, api_key: str = "", sender: str = ""):
self.endpoint = endpoint
self.api_key = api_key
self.sender = sender
def send(self, recipient: str, message: str) -> str:
payload = {"to": recipient, "text": message, "from": self.sender, "api_key": self.api_key}
data = json.dumps(payload).encode("utf-8")
request = urllib.request.Request(
self.endpoint, data=data, headers={"Content-Type": "application/json"}, method="POST"
)
with urllib.request.urlopen(request, timeout=15) as response:
return response.read().decode("utf-8", "replace")[:120]
def get_sms_provider() -> SmsProviderInterface:
service = SettingsService()
provider = service.get("sms.provider", "mock")
if provider == "http":
return HttpSmsProvider(
endpoint=service.get("sms.endpoint", ""),
api_key=service.get("sms.api_key", ""),
sender=service.get("sms.sender", ""),
)
return MockSmsProvider()
def render_template(template_text: str, context: dict) -> str:
def _replace(match):
key = match.group(1)
return str(context.get(key, ""))
return re.sub(r"\{\{\s*(\w+)\s*\}\}", _replace, template_text)
+39
View File
@@ -0,0 +1,39 @@
"""Notification engine.
Resolves templates for a domain event from the database, renders them and
dispatches through the configured provider. Business logic only emits events.
"""
from django.utils import timezone
from apps.notifications.models import NotificationLog, NotificationTemplate
from apps.notifications.providers import get_sms_provider, render_template
class NotificationService:
def send_event(self, event: str, context: dict, recipient: str = "") -> list:
results = []
templates = NotificationTemplate.objects.filter(event=event, enabled=True)
for template in templates:
body = render_template(template.body, context)
if template.channel == NotificationTemplate.Channel.SMS:
results.append(self._send_sms(template, recipient, body))
return results
def _send_sms(self, template, recipient: str, body: str) -> dict:
status = "sent"
error = ""
reference = ""
try:
reference = get_sms_provider().send(recipient, body)
except Exception as exc: # pragma: no cover - provider failures
status = "failed"
error = str(exc)
NotificationLog.objects.create(
template=template,
recipient=recipient,
channel="sms",
status=status,
error=error,
sent_at=timezone.now() if status == "sent" else None,
)
return {"channel": "sms", "status": status, "reference": reference}