// Package routing is the recipient resolution + delivery enqueue step. // M1: broadcast mode — every alert to a company goes to every active // fcm_token of every active individual in that company. M2 replaces // this with the rules engine from SPEC §6 (subscriptions, opt-in, // quiet hours, routing rules, …). // // The interface is small: ResolveTokens returns the set of // (individual_id, fcm_token, locale) tuples that should receive // the alert. deliverd-fcm is the only consumer in M1. package routing import ( "context" "fmt" "git3.techno-world.net/lrosales/broad-announce/internal/postgres" ) // Target is one device to deliver to. type Target struct { IndividualID string FCMToken string Locale string } // Resolver looks up recipients for a (company, alert) pair. type Resolver struct { pool *postgres.Pool } // New constructs a Resolver. func New(pool *postgres.Pool) *Resolver { return &Resolver{pool: pool} } // ResolveTokens returns every active FCM token for every active // individual in the given company. This is the M1 broadcast path. // M2 swaps this for the rules engine. // // One DB round-trip via a single join. Add a per-company limit // here if a malicious company can ever register 1M tokens. func (r *Resolver) ResolveTokens(ctx context.Context, companyID string) ([]Target, error) { rows, err := r.pool.Query(ctx, ` SELECT i.id, t.token, COALESCE(t.locale, i.locale, 'en') FROM individuals i JOIN fcm_tokens t ON t.individual_id = i.id WHERE i.company_id = $1 AND i.status = 'active' AND t.status = 'active' ORDER BY i.id, t.id `, companyID) if err != nil { return nil, fmt.Errorf("resolve tokens: %w", err) } defer rows.Close() var out []Target for rows.Next() { var t Target if err := rows.Scan(&t.IndividualID, &t.FCMToken, &t.Locale); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() }