package license import ( "context" "errors" "fmt" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ( ErrNoLicense = errors.New("license: kein lizenzdatensatz fuer diesen tenant") ErrLicenseExpired = errors.New("license: lizenz abgelaufen") ) // Store persistiert den Lizenzumfang je Tenant in der Control-Plane-Registry // (siehe internal/tenant.Registry — dieselbe Datenbank, aber ein eigener, // unabhaengiger Store, um internal/tenant nicht um lizenzfremde Belange zu // erweitern). type Store struct { pool *pgxpool.Pool validator *Validator } func NewStore(pool *pgxpool.Pool, validator *Validator) *Store { return &Store{pool: pool, validator: validator} } // Install prueft die Signatur des Lizenzschluessels (Akzeptanzkriterium 1) // und ersetzt den bisherigen Lizenzdatensatz des Tenants vollstaendig. Ein // bereits abgelaufener, aber korrekt signierter Schluessel wird trotzdem // gespeichert — der Ablauf wird erst bei der Nutzung (RequireActive) // bewertet, nicht beim Einspielen. func (s *Store) Install(ctx context.Context, tenantID, licenseKey string) (Payload, error) { payload, err := s.validator.Parse(licenseKey) if err != nil { return Payload{}, err } _, err = s.pool.Exec(ctx, ` INSERT INTO tenant_licenses (tenant_id, plan, modules, issued_at, valid_until, raw_key, installed_at) VALUES ($1, $2, $3, $4, $5, $6, now()) ON CONFLICT (tenant_id) DO UPDATE SET plan = $2, modules = $3, issued_at = $4, valid_until = $5, raw_key = $6, installed_at = now() `, tenantID, payload.Plan, payload.Modules, payload.IssuedAt, payload.ValidUntil, licenseKey) if err != nil { return Payload{}, fmt.Errorf("lizenz speichern: %w", err) } return payload, nil } func (s *Store) get(ctx context.Context, tenantID string) (Payload, error) { var p Payload row := s.pool.QueryRow(ctx, ` SELECT plan, modules, issued_at, valid_until FROM tenant_licenses WHERE tenant_id = $1 `, tenantID) if err := row.Scan(&p.Plan, &p.Modules, &p.IssuedAt, &p.ValidUntil); err != nil { if errors.Is(err, pgx.ErrNoRows) { return Payload{}, ErrNoLicense } return Payload{}, fmt.Errorf("lizenz lesen: %w", err) } return p, nil } // Status liefert den persistierten Lizenzumfang unabhaengig vom Ablauf // (Akzeptanzkriterium 3: Plan, Modul-Liste, Laufzeit abfragbar). func (s *Store) Status(ctx context.Context, tenantID string) (Payload, error) { return s.get(ctx, tenantID) } // RequireActive liefert den Lizenzumfang NUR, wenn die Lizenz noch nicht // abgelaufen ist — sonst ErrLicenseExpired statt eines harten Fehlers/Panics // (Akzeptanzkriterium 2: definierter eingeschraenkter Zustand). Aufrufende // Module (LIC-02/03) entscheiden, was "eingeschraenkt" konkret bedeutet. func (s *Store) RequireActive(ctx context.Context, tenantID string) (Payload, error) { p, err := s.get(ctx, tenantID) if err != nil { return Payload{}, err } if time.Now().After(p.ValidUntil) { return Payload{}, ErrLicenseExpired } return p, nil }