package main import ( "encoding/json" "io" "net/http" "git.k6n.net/mats/go-redis-inventory/pkg/inventory" ) // batchItem is one stock set in a batch request. LocationID is optional; an // empty value defaults to the service's country location (the same location the // catalog-feed listener writes to). type batchItem struct { SKU string `json:"sku"` LocationID string `json:"locationId,omitempty"` Quantity int64 `json:"quantity"` } // batchResult mirrors one input item with the location actually written and an // optional per-item error (empty on success). type batchResult struct { SKU string `json:"sku"` LocationID string `json:"locationId"` Quantity int64 `json:"quantity"` Error string `json:"error,omitempty"` } // batchInventoryHandler SETS stock to absolute values for many SKUs in one // pipelined call — for re-seeding/import and for testing the inventory + reserve // paths directly. Same write as the catalog-feed listener (UpdateInventory + // level crossing), so a value set here is authoritative until the next // catalog.item_changed for that SKU overwrites it (catalog is the source of // truth). Body is a JSON array of {sku, locationId?, quantity}. func (srv *Server) batchInventoryHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 16<<20)) // 16 MiB if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } var items []batchItem if err := json.Unmarshal(body, &items); err != nil { http.Error(w, "body must be a JSON array of {sku, locationId?, quantity}: "+err.Error(), http.StatusBadRequest) return } if len(items) == 0 { http.Error(w, "empty batch", http.StatusBadRequest) return } ctx := r.Context() results := make([]batchResult, len(items)) pipe := srv.rdb.Pipeline() for i, it := range items { loc := it.LocationID if loc == "" { loc = country } results[i] = batchResult{SKU: it.SKU, LocationID: loc, Quantity: it.Quantity} if it.SKU == "" { results[i].Error = "sku is required" continue } srv.inventoryService.UpdateInventory(ctx, pipe, inventory.SKU(it.SKU), inventory.LocationID(loc), it.Quantity) } hadError := false if _, err := pipe.Exec(ctx); err != nil { // Exec reports the first failing command; flag the whole batch rather than // guess which items committed. hadError = true for i := range results { if results[i].Error == "" { results[i].Error = err.Error() } } } else { // Emit level crossings for the items that were actually written, so the // finder/level listeners react the same way they do for catalog feeds. for i := range results { if results[i].Error == "" { emitLevelIfChanged(ctx, srv.rdb, srv.conn, country, lowWatermark, results[i].SKU, results[i].LocationID, results[i].Quantity) } } } for i := range results { if results[i].Error != "" { hadError = true break } } status := http.StatusOK if hadError { status = http.StatusMultiStatus // 207 } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]any{"count": len(results), "results": results}) }