diff --git a/pkg/backofficeadmin/app.go b/pkg/backofficeadmin/app.go index 5cb6217..a38230c 100644 --- a/pkg/backofficeadmin/app.go +++ b/pkg/backofficeadmin/app.go @@ -58,9 +58,10 @@ type Config struct { // App is the commerce admin control plane. Construct with New, register its // HTTP surface with RegisterRoutes, run background work with Start. type App struct { - fs *FileServer - hub *Hub - cs *CustomerServer + fs *FileServer + hub *Hub + cs *CustomerServer + OnVouchersChange func(codes []string) } // New constructs the commerce admin: it opens the cart/checkout disk event-log @@ -171,6 +172,7 @@ func New(cfg Config) (*App, error) { // apply auth or CORS — the composing host (or standalone main) wraps the mux // with those. func (a *App) RegisterRoutes(mux *http.ServeMux) { + a.fs.OnVouchersChange = a.OnVouchersChange mux.HandleFunc("GET /carts", a.fs.CartsHandler) mux.HandleFunc("GET /cart/{id}", a.fs.CartHandler) mux.HandleFunc("GET /checkouts", a.fs.CheckoutsHandler) diff --git a/pkg/backofficeadmin/fileserver.go b/pkg/backofficeadmin/fileserver.go index f267737..9f4766d 100644 --- a/pkg/backofficeadmin/fileserver.go +++ b/pkg/backofficeadmin/fileserver.go @@ -24,11 +24,12 @@ import ( type FileServer struct { // Define fields here - dataDir string - checkoutDataDir string - promotionsFile string - storage actor.LogStorage[cart.CartGrain] - checkoutStorage actor.LogStorage[checkout.CheckoutGrain] + dataDir string + checkoutDataDir string + promotionsFile string + storage actor.LogStorage[cart.CartGrain] + checkoutStorage actor.LogStorage[checkout.CheckoutGrain] + OnVouchersChange func(codes []string) } func NewFileServer(dataDir string, checkoutDataDir string, promotionsFile string, storage actor.LogStorage[cart.CartGrain], checkoutStorage actor.LogStorage[checkout.CheckoutGrain]) *FileServer { @@ -276,15 +277,37 @@ func (fs *FileServer) VoucherHandler(w http.ResponseWriter, r *http.Request) { return } if r.Method == http.MethodPost { - _ = os.MkdirAll(filepath.Dir(fileName), 0755) - file, err := os.Create(fileName) + body, err := io.ReadAll(r.Body) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } - defer file.Close() - io.Copy(file, r.Body) + var parsed struct { + State struct { + Vouchers []struct { + Code string `json:"code"` + } `json:"vouchers"` + } `json:"state"` + } + var codes []string + if err := json.Unmarshal(body, &parsed); err == nil { + for _, v := range parsed.State.Vouchers { + if v.Code != "" { + codes = append(codes, v.Code) + } + } + } + + _ = os.MkdirAll(filepath.Dir(fileName), 0755) + if err := os.WriteFile(fileName, body, 0644); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + if fs.OnVouchersChange != nil { + fs.OnVouchersChange(codes) + } return } diff --git a/pkg/promotions/store.go b/pkg/promotions/store.go index 976f223..adf7541 100644 --- a/pkg/promotions/store.go +++ b/pkg/promotions/store.go @@ -18,6 +18,7 @@ type Store struct { path string version int promotions []PromotionRule + OnChange func(id string, isDelete bool) } // NewStore loads the state file at path (an absent file yields an empty store) @@ -72,40 +73,72 @@ func (s *Store) Upsert(rule PromotionRule) (replaced bool, err error) { if rule.ID == "" { return false, fmt.Errorf("promotion id is required") } + var cb func(string, bool) s.mu.Lock() - defer s.mu.Unlock() + defer func() { + s.mu.Unlock() + if err == nil && cb != nil { + cb(rule.ID, false) + } + }() + cb = s.OnChange + for i := range s.promotions { if s.promotions[i].ID == rule.ID { s.promotions[i] = rule - return true, s.saveLocked() + replaced = true + err = s.saveLocked() + return } } s.promotions = append(s.promotions, rule) - return false, s.saveLocked() + replaced = false + err = s.saveLocked() + return } // 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) { +func (s *Store) SetStatus(id string, status PromotionStatus) (found bool, err error) { + var cb func(string, bool) s.mu.Lock() - defer s.mu.Unlock() + defer func() { + s.mu.Unlock() + if err == nil && found && cb != nil { + cb(id, false) + } + }() + cb = s.OnChange + for i := range s.promotions { if s.promotions[i].ID == id { s.promotions[i].Status = status - return true, s.saveLocked() + found = true + err = s.saveLocked() + return } } 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) { +func (s *Store) Delete(id string) (found bool, err error) { + var cb func(string, bool) s.mu.Lock() - defer s.mu.Unlock() + defer func() { + s.mu.Unlock() + if err == nil && found && cb != nil { + cb(id, true) + } + }() + cb = s.OnChange + 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() + found = true + err = s.saveLocked() + return } } return false, nil