mcp in cart
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package promotions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Store is a thread-safe, file-backed collection of promotion rules. It is the
|
||||
// single source of truth shared by the cart's mutation processor (which reads a
|
||||
// snapshot on every recompute) and the promotion MCP tools (which mutate it and
|
||||
// persist to disk). Because the cart re-reads the snapshot each time totals are
|
||||
// recomputed, MCP edits take effect on the next cart mutation — no restart.
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
version int
|
||||
promotions []PromotionRule
|
||||
}
|
||||
|
||||
// NewStore loads the state file at path (an absent file yields an empty store)
|
||||
// and returns a Store that persists back to the same path.
|
||||
func NewStore(path string) (*Store, error) {
|
||||
sf, err := LoadStateFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version := sf.Version
|
||||
if version == 0 {
|
||||
version = 1
|
||||
}
|
||||
return &Store{
|
||||
path: path,
|
||||
version: version,
|
||||
promotions: sf.State.Promotions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Snapshot returns a copy of the current rules, safe to evaluate without holding
|
||||
// the lock. The PromotionRule values are passed by value to EvaluateAll, so a
|
||||
// shallow copy of the slice is enough to avoid racing with concurrent mutations.
|
||||
func (s *Store) Snapshot() []PromotionRule {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make([]PromotionRule, len(s.promotions))
|
||||
copy(out, s.promotions)
|
||||
return out
|
||||
}
|
||||
|
||||
// List returns a copy of all rules (for the list_promotions tool).
|
||||
func (s *Store) List() []PromotionRule {
|
||||
return s.Snapshot()
|
||||
}
|
||||
|
||||
// Get returns the rule with the given id.
|
||||
func (s *Store) Get(id string) (PromotionRule, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, r := range s.promotions {
|
||||
if r.ID == id {
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
return PromotionRule{}, false
|
||||
}
|
||||
|
||||
// Upsert inserts the rule or replaces the existing one with the same ID, then
|
||||
// persists. Returns true if an existing rule was replaced.
|
||||
func (s *Store) Upsert(rule PromotionRule) (replaced bool, err error) {
|
||||
if rule.ID == "" {
|
||||
return false, fmt.Errorf("promotion id is required")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for i := range s.promotions {
|
||||
if s.promotions[i].ID == rule.ID {
|
||||
s.promotions[i] = rule
|
||||
return true, s.saveLocked()
|
||||
}
|
||||
}
|
||||
s.promotions = append(s.promotions, rule)
|
||||
return false, s.saveLocked()
|
||||
}
|
||||
|
||||
// SetStatus changes the status of a rule and persists. Returns false if no rule
|
||||
// with the id exists.
|
||||
func (s *Store) SetStatus(id string, status PromotionStatus) (bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for i := range s.promotions {
|
||||
if s.promotions[i].ID == id {
|
||||
s.promotions[i].Status = status
|
||||
return true, s.saveLocked()
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Delete removes the rule with the id and persists. Returns false if not found.
|
||||
func (s *Store) Delete(id string) (bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for i := range s.promotions {
|
||||
if s.promotions[i].ID == id {
|
||||
s.promotions = append(s.promotions[:i], s.promotions[i+1:]...)
|
||||
return true, s.saveLocked()
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// saveLocked writes the current state to disk atomically. Caller holds s.mu.
|
||||
func (s *Store) saveLocked() error {
|
||||
sf := StateFile{
|
||||
Version: s.version,
|
||||
State: PromotionState{Promotions: s.promotions},
|
||||
}
|
||||
data, err := json.MarshalIndent(sf, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(s.path)
|
||||
tmp, err := os.CreateTemp(dir, ".promotions-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
defer os.Remove(tmpName) // no-op once renamed
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpName, s.path)
|
||||
}
|
||||
Reference in New Issue
Block a user