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
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>
91 lines
3.5 KiB
Go
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
|
|
}
|