routing.go 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. // Package routing is the recipient resolution + delivery enqueue step.
  2. // M1: broadcast mode — every alert to a company goes to every active
  3. // fcm_token of every active individual in that company. M2 replaces
  4. // this with the rules engine from SPEC §6 (subscriptions, opt-in,
  5. // quiet hours, routing rules, …).
  6. //
  7. // The interface is small: ResolveTokens returns the set of
  8. // (individual_id, fcm_token, locale) tuples that should receive
  9. // the alert. deliverd-fcm is the only consumer in M1.
  10. package routing
  11. import (
  12. "context"
  13. "fmt"
  14. "git3.techno-world.net/lrosales/broad-announce/internal/postgres"
  15. )
  16. // Target is one device to deliver to.
  17. type Target struct {
  18. IndividualID string
  19. FCMToken string
  20. Locale string
  21. }
  22. // Resolver looks up recipients for a (company, alert) pair.
  23. type Resolver struct {
  24. pool *postgres.Pool
  25. }
  26. // New constructs a Resolver.
  27. func New(pool *postgres.Pool) *Resolver { return &Resolver{pool: pool} }
  28. // ResolveTokens returns every active FCM token for every active
  29. // individual in the given company. This is the M1 broadcast path.
  30. // M2 swaps this for the rules engine.
  31. //
  32. // One DB round-trip via a single join. Add a per-company limit
  33. // here if a malicious company can ever register 1M tokens.
  34. func (r *Resolver) ResolveTokens(ctx context.Context, companyID string) ([]Target, error) {
  35. rows, err := r.pool.Query(ctx, `
  36. SELECT i.id, t.token, COALESCE(t.locale, i.locale, 'en')
  37. FROM individuals i
  38. JOIN fcm_tokens t ON t.individual_id = i.id
  39. WHERE i.company_id = $1
  40. AND i.status = 'active'
  41. AND t.status = 'active'
  42. ORDER BY i.id, t.id
  43. `, companyID)
  44. if err != nil {
  45. return nil, fmt.Errorf("resolve tokens: %w", err)
  46. }
  47. defer rows.Close()
  48. var out []Target
  49. for rows.Next() {
  50. var t Target
  51. if err := rows.Scan(&t.IndividualID, &t.FCMToken, &t.Locale); err != nil {
  52. return nil, err
  53. }
  54. out = append(out, t)
  55. }
  56. return out, rows.Err()
  57. }