package services import ( "context" "fmt" "github.com/google/uuid" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/KanzlAI-mGMT/internal/models" ) type BillingRateService struct { db *sqlx.DB audit *AuditService } func NewBillingRateService(db *sqlx.DB, audit *AuditService) *BillingRateService { return &BillingRateService{db: db, audit: audit} } type UpsertBillingRateInput struct { UserID *uuid.UUID `json:"user_id,omitempty"` Rate float64 `json:"rate"` Currency string `json:"currency"` ValidFrom string `json:"valid_from"` ValidTo *string `json:"valid_to,omitempty"` } func (s *BillingRateService) List(ctx context.Context, tenantID uuid.UUID) ([]models.BillingRate, error) { var rates []models.BillingRate err := s.db.SelectContext(ctx, &rates, `SELECT id, tenant_id, user_id, rate, currency, valid_from, valid_to, created_at FROM billing_rates WHERE tenant_id = $1 ORDER BY valid_from DESC, user_id NULLS LAST`, tenantID) if err != nil { return nil, fmt.Errorf("list billing rates: %w", err) } return rates, nil } func (s *BillingRateService) Upsert(ctx context.Context, tenantID uuid.UUID, input UpsertBillingRateInput) (*models.BillingRate, error) { if input.Currency == "" { input.Currency = "EUR" } // Close any existing open-ended rate for this user _, err := s.db.ExecContext(ctx, `UPDATE billing_rates SET valid_to = $3 WHERE tenant_id = $1 AND (($2::uuid IS NULL AND user_id IS NULL) OR user_id = $2) AND valid_to IS NULL AND valid_from < $3`, tenantID, input.UserID, input.ValidFrom) if err != nil { return nil, fmt.Errorf("close existing rate: %w", err) } var rate models.BillingRate err = s.db.QueryRowxContext(ctx, `INSERT INTO billing_rates (tenant_id, user_id, rate, currency, valid_from, valid_to) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, tenant_id, user_id, rate, currency, valid_from, valid_to, created_at`, tenantID, input.UserID, input.Rate, input.Currency, input.ValidFrom, input.ValidTo, ).StructScan(&rate) if err != nil { return nil, fmt.Errorf("upsert billing rate: %w", err) } s.audit.Log(ctx, "create", "billing_rate", &rate.ID, nil, rate) return &rate, nil } func (s *BillingRateService) GetCurrentRate(ctx context.Context, tenantID uuid.UUID, userID uuid.UUID, date string) (*float64, error) { var rate float64 err := s.db.GetContext(ctx, &rate, `SELECT rate FROM billing_rates WHERE tenant_id = $1 AND (user_id = $2 OR user_id IS NULL) AND valid_from <= $3 AND (valid_to IS NULL OR valid_to >= $3) ORDER BY user_id NULLS LAST LIMIT 1`, tenantID, userID, date) if err != nil { return nil, err } return &rate, nil }