3f8bc58ca9
Phase 1 — Core API (Go): - Events, guests, tokens, RSVPs CRUD on PostgreSQL via pgx/v5 - HMAC-signed per-guest tokens with format validation - Health endpoint with DB ping, slog JSON logging, graceful shutdown Phase 2 — NATS + Fraud Engine: - NATS JetStream pub/sub with explicit-ack consumers - Python/FastAPI fraud engine with heuristic risk scoring (fingerprint mismatch, IP change, missing signals, repeated access) - gRPC sync scoring with 250ms fail-open timeout - Per-guest baseline tracking; risk bands low/medium/high/block Phase 3 — Notifications + Frontend: - Notification worker scaffolding (Twilio/SES stubs, retry/backoff) - Nuxt 3 frontend with Tailwind dark theme + brand green - Live monitor via WebSocket with auto-reconnect - Activity history endpoint backfills monitor with RSVPs + scored access checks (including blocked attempts) UX polish: - Marketing-friendly landing page (hero mockup, how-it-works, features, use cases, testimonials, FAQ, final CTA) - Animated layered card mockups on landing + new-event page - Plus-ones stepper, RSVP status badges, filter buttons - Friendly access-check labels (Verified/Review/Suspicious/Blocked) - Dashboard hydration fix via ClientOnly wrapper Infrastructure: - docker-compose for full local dev (postgres, nats, api, fraud-engine, notifier, frontend) - Multi-stage Dockerfiles, non-root UID 1000 - Integration tests with testcontainers-go Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
123 lines
2.7 KiB
Go
123 lines
2.7 KiB
Go
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
|
|
}
|