ProofedMesh/services/admin-api/internal/store/memory.go

149 lines
3.6 KiB
Go
Raw Permalink Normal View History

package store
import (
"context"
"fmt"
"sort"
"sync"
"time"
"basicstack.de/proofedmesh/services/admin-api/internal/model"
)
// Memory is a thread-safe in-memory implementation of RateStore. It is the
// fastest path for tests and the dev loop (`-store=memory`). Production uses
// the Postgres implementation.
type Memory struct {
mu sync.Mutex
rates map[string]model.Rate
seq int
}
func NewMemory() *Memory {
return &Memory{rates: make(map[string]model.Rate)}
}
func (m *Memory) List(ctx context.Context, f ListFilter) ([]model.Rate, error) {
m.mu.Lock()
defer m.mu.Unlock()
today := time.Now().UTC().Truncate(24 * time.Hour)
out := make([]model.Rate, 0, len(m.rates))
for _, r := range m.rates {
if f.Country != "" && r.Country != f.Country {
continue
}
if f.ActiveOnly {
if r.ValidTo != nil && r.ValidTo.Before(today) {
continue
}
}
out = append(out, r)
}
sort.Slice(out, func(i, j int) bool { return out[i].RateID < out[j].RateID })
return out, nil
}
func (m *Memory) Get(ctx context.Context, rateID string) (model.Rate, error) {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.rates[rateID]
if !ok {
return model.Rate{}, ErrNotFound
}
return r, nil
}
func (m *Memory) Create(ctx context.Context, in model.NewRateInput, createdBy string, now time.Time) (model.Rate, error) {
m.mu.Lock()
defer m.mu.Unlock()
if err := checkOverlap(m.rates, in); err != nil {
return model.Rate{}, err
}
m.seq++
r := model.Rate{
RateID: fmt.Sprintf("PDR-%d", m.seq),
Country: in.Country,
City: in.City,
Currency: in.Currency,
FullDay: in.FullDay,
PartialDayMin8h: in.PartialDayMin8h,
ArrivalDay: in.ArrivalDay,
DepartureDay: in.DepartureDay,
ValidFrom: in.ValidFrom,
ValidTo: in.ValidTo,
SourceRegulation: in.SourceRegulation,
CreatedBy: createdBy,
CreatedAt: now,
}
m.rates[r.RateID] = r
return r, nil
}
func (m *Memory) Retire(ctx context.Context, rateID string, validTo time.Time, performedBy string, now time.Time) (model.Rate, error) {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.rates[rateID]
if !ok {
return model.Rate{}, ErrNotFound
}
today := now.UTC().Truncate(24 * time.Hour)
if r.ValidTo != nil && r.ValidTo.Before(today) {
return model.Rate{}, ErrAlreadyRetired
}
if validTo.Before(r.ValidFrom) {
return model.Rate{}, fmt.Errorf("retire: valid_to %s is before valid_from %s", validTo.Format("2006-01-02"), r.ValidFrom.Format("2006-01-02"))
}
cpy := r
cpy.ValidTo = &validTo
m.rates[rateID] = cpy
return cpy, nil
}
func (m *Memory) AttachAttestation(ctx context.Context, rateID string, attestationRef string) error {
m.mu.Lock()
defer m.mu.Unlock()
r, ok := m.rates[rateID]
if !ok {
return ErrNotFound
}
if !model.IsAttestationRef(attestationRef) {
return fmt.Errorf("attestation ref %q is not a sha256: content hash", attestationRef)
}
cpy := r
cpy.RateChangeAttestationRef = &attestationRef
m.rates[rateID] = cpy
return nil
}
func checkOverlap(existing map[string]model.Rate, in model.NewRateInput) error {
cityKey := ""
if in.City != nil {
cityKey = *in.City
}
newEnd := time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)
if in.ValidTo != nil {
newEnd = *in.ValidTo
}
for _, r := range existing {
if r.Country != in.Country {
continue
}
rc := ""
if r.City != nil {
rc = *r.City
}
if rc != cityKey {
continue
}
rEnd := time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)
if r.ValidTo != nil {
rEnd = *r.ValidTo
}
// Overlapping ranges? Inclusive on both ends.
if !in.ValidFrom.After(rEnd) && !r.ValidFrom.After(newEnd) {
return ErrOverlap
}
}
return nil
}