package routing import ( "context" "fmt" "sort" "sync" "time" ) // RuleStore is the persistence interface for routing rules. // The MemStore implements it for tests; PgStore implements it for production. type RuleStore interface { // ListActive returns all enabled rules for tenantID, sorted by priority ASC. ListActive(ctx context.Context, tenantID string) ([]RoutingRule, error) // Get returns a single rule by id, scoped to tenantID. Get(ctx context.Context, id, tenantID string) (RoutingRule, error) // Create persists a new rule and returns the created record. Create(ctx context.Context, rule RoutingRule) (RoutingRule, error) // Update replaces an existing rule and returns the updated record. Update(ctx context.Context, rule RoutingRule) (RoutingRule, error) // Delete permanently removes a rule. Only affects the given tenantID. Delete(ctx context.Context, id, tenantID string) error } // ErrNotFound is returned by Get/Update/Delete when the rule does not exist. var ErrNotFound = fmt.Errorf("routing rule not found") // ─── MemStore — in-memory implementation (tests & dev) ────────────────────── // MemStore is a thread-safe in-memory RuleStore for unit tests. type MemStore struct { mu sync.RWMutex rules map[string]RoutingRule // id → rule seq int } // NewMemStore creates an empty MemStore. func NewMemStore() *MemStore { return &MemStore{rules: make(map[string]RoutingRule)} } func (m *MemStore) ListActive(_ context.Context, tenantID string) ([]RoutingRule, error) { m.mu.RLock() defer m.mu.RUnlock() var out []RoutingRule for _, r := range m.rules { if r.TenantID == tenantID && r.IsEnabled { out = append(out, r) } } sort.Slice(out, func(i, j int) bool { return out[i].Priority < out[j].Priority }) return out, nil } func (m *MemStore) Get(_ context.Context, id, tenantID string) (RoutingRule, error) { m.mu.RLock() defer m.mu.RUnlock() r, ok := m.rules[id] if !ok || r.TenantID != tenantID { return RoutingRule{}, ErrNotFound } return r, nil } func (m *MemStore) Create(_ context.Context, rule RoutingRule) (RoutingRule, error) { m.mu.Lock() defer m.mu.Unlock() m.seq++ rule.ID = fmt.Sprintf("mem-%d", m.seq) rule.CreatedAt = time.Now() rule.UpdatedAt = time.Now() m.rules[rule.ID] = rule return rule, nil } func (m *MemStore) Update(_ context.Context, rule RoutingRule) (RoutingRule, error) { m.mu.Lock() defer m.mu.Unlock() existing, ok := m.rules[rule.ID] if !ok || existing.TenantID != rule.TenantID { return RoutingRule{}, ErrNotFound } rule.CreatedAt = existing.CreatedAt rule.UpdatedAt = time.Now() m.rules[rule.ID] = rule return rule, nil } func (m *MemStore) Delete(_ context.Context, id, tenantID string) error { m.mu.Lock() defer m.mu.Unlock() r, ok := m.rules[id] if !ok || r.TenantID != tenantID { return ErrNotFound } delete(m.rules, id) return nil }