package notification import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/alchemistkay/guestguard/internal/storage" ) type Channel string const ( ChannelSMS Channel = "sms" ChannelEmail Channel = "email" ) type Type string const ( TypeInvitation Type = "invitation" TypeVerification Type = "verification" TypeConfirmation Type = "confirmation" TypeReminder Type = "reminder" ) type Status string const ( StatusQueued Status = "queued" StatusSent Status = "sent" StatusDelivered Status = "delivered" StatusFailed Status = "failed" ) // Sender is the boundary between the worker and a real provider (Twilio, // SES, etc). Phase 3 ships a logging implementation; later phases swap it // out without touching consumer code. type Sender interface { Send(ctx context.Context, msg OutboundMessage) (providerID string, err error) } type OutboundMessage struct { GuestID uuid.UUID Channel Channel Type Type Subject string Body string Metadata map[string]any } // Repo persists notification records. type Repo struct { pool *pgxpool.Pool } func NewRepo(db *storage.DB) *Repo { return &Repo{pool: db.Pool} } type RecordParams struct { GuestID uuid.UUID Channel Channel Type Type Status Status ProviderID string Error string } func (r *Repo) Record(ctx context.Context, p RecordParams) (uuid.UUID, error) { var providerID *string if p.ProviderID != "" { providerID = &p.ProviderID } var errStr *string if p.Error != "" { errStr = &p.Error } var deliveredAt *time.Time if p.Status == StatusSent || p.Status == StatusDelivered { now := time.Now().UTC() deliveredAt = &now } const q = ` INSERT INTO notifications (guest_id, channel, type, status, provider_id, attempts, last_attempt, delivered_at, error) VALUES ($1, $2, $3, $4, $5, 1, now(), $6, $7) RETURNING id ` var id uuid.UUID err := r.pool.QueryRow(ctx, q, p.GuestID, string(p.Channel), string(p.Type), string(p.Status), providerID, deliveredAt, errStr, ).Scan(&id) if err != nil { return uuid.Nil, fmt.Errorf("record notification: %w", err) } return id, nil } // LogSender pretends to send and just logs. Useful for Phase 3 demos and // tests; concrete providers (Twilio/SES) plug in later. type LogSender struct{} func (LogSender) Send(_ context.Context, msg OutboundMessage) (string, error) { if msg.GuestID == uuid.Nil { return "", errors.New("missing guest id") } meta, _ := json.Marshal(msg.Metadata) providerID := "log:" + uuid.NewString() // We deliberately don't write to stdout here; the worker emits the slog // line so we control the structure. _ = meta return providerID, nil }