Files
soroush.asadi 507ac7e6a4
Build backend images / build content-svc (push) Failing after 56s
Build backend images / build file-svc (push) Failing after 47s
Build backend images / build gateway (push) Failing after 1m0s
Build backend images / build identity-svc (push) Failing after 56s
Build backend images / build notification-svc (push) Failing after 11s
Build backend images / build render-svc (push) Failing after 4m5s
Build backend images / build studio-svc (push) Failing after 56s
feat(notifications+admin): SMS (Kavenegar) + Email (SMTP) channels & templates
Backend (notification-svc):
- channel_config table (per-tenant Kavenegar + SMTP settings) + migration 18
- sender pkg: Kavenegar SMS client + SMTP mailer (STARTTLS / implicit TLS), stdlib only
- endpoints: GET/PUT /v1/channels[/:channel], POST /v1/sms/send, POST /v1/email/send
  (template + {{var}} rendering); deliveries logged
- seeded 3 Persian email templates: welcome / account_verification / promotion
- gateway routes /v1/{channels,sms,email}/* → notification

Admin UI:
- /admin/messaging: SMS + Email provider config cards, test-send, email template editor
- nav link + fa/en labels

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 17:32:54 +03:30

91 lines
3.5 KiB
Go

package db
import (
"context"
"encoding/json"
"github.com/flatrender/notification-svc/internal/models"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
// ── Channel provider config ──────────────────────────────────────────────────
func (s *Store) GetChannelConfigs(ctx context.Context, tenantID uuid.UUID) ([]*models.ChannelConfig, error) {
rows, err := s.pool.Query(ctx,
`SELECT tenant_id, channel, settings, enabled, updated_at
FROM notification.channel_config WHERE tenant_id = $1 ORDER BY channel`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*models.ChannelConfig
for rows.Next() {
c := &models.ChannelConfig{}
var raw []byte
if err := rows.Scan(&c.TenantID, &c.Channel, &raw, &c.Enabled, &c.UpdatedAt); err != nil {
return nil, err
}
_ = json.Unmarshal(raw, &c.Settings)
out = append(out, c)
}
return out, rows.Err()
}
func (s *Store) GetChannelConfig(ctx context.Context, tenantID uuid.UUID, channel string) (*models.ChannelConfig, error) {
c := &models.ChannelConfig{}
var raw []byte
err := s.pool.QueryRow(ctx,
`SELECT tenant_id, channel, settings, enabled, updated_at
FROM notification.channel_config WHERE tenant_id = $1 AND channel = $2`, tenantID, channel).
Scan(&c.TenantID, &c.Channel, &raw, &c.Enabled, &c.UpdatedAt)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
_ = json.Unmarshal(raw, &c.Settings)
return c, nil
}
func (s *Store) UpsertChannelConfig(ctx context.Context, tenantID uuid.UUID, channel string, settings map[string]any, enabled bool) error {
raw, _ := json.Marshal(settings)
_, err := s.pool.Exec(ctx,
`INSERT INTO notification.channel_config (tenant_id, channel, settings, enabled, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (tenant_id, channel)
DO UPDATE SET settings = EXCLUDED.settings, enabled = EXCLUDED.enabled, updated_at = NOW()`,
tenantID, channel, raw, enabled)
return err
}
// ── Email template lookup ────────────────────────────────────────────────────
func (s *Store) GetEmailTemplate(ctx context.Context, code, locale string) (*models.NotificationTemplate, error) {
t := &models.NotificationTemplate{}
err := s.pool.QueryRow(ctx,
`SELECT id, code, channel, locale, subject, body_text, body_html, is_active
FROM notification.notification_templates
WHERE code = $1 AND channel = 'Email' AND locale = $2 AND is_active = TRUE
LIMIT 1`, code, locale).
Scan(&t.ID, &t.Code, &t.Channel, &t.Locale, &t.Subject, &t.BodyText, &t.BodyHTML, &t.IsActive)
if err == pgx.ErrNoRows {
return nil, nil
}
return t, err
}
// ── Delivery log ─────────────────────────────────────────────────────────────
func (s *Store) LogDelivery(ctx context.Context, tenantID, userID uuid.UUID, channel, recipient string,
subject, provider, providerMsgID, status, errMsg *string) error {
_, err := s.pool.Exec(ctx,
`INSERT INTO notification.notification_deliveries
(tenant_id, user_id, channel, recipient, subject, provider, provider_message_id, status, error_message, sent_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())`,
tenantID, userID, channel, recipient, subject, provider, providerMsgID, status, errMsg)
return err
}