This commit is contained in:
mats committed 2026-06-28 17:52:49 +02:00
1 parent e706f7d907
commit 21bcb63440
42 files changed
+5903

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
// Package inventory is the platform's shared inventory seam: the cross-module
// vocabulary for stock *levels*. It deliberately carries no quantities — exact
// stock is queried synchronously from the inventory service at decision points
// (PDP, add-to-cart, checkout reserve), and only coarse level crossings travel
// over the bus. Putting exact quantity on the bus spams it and ships a number
// that is stale the moment it is published; a level crossing is the only
// inventory signal other modules receive. See docs/inventory-shape.md.
//
// Like platform/catalog, this package holds only the wire value types plus the
// single classification function, so the producer and every consumer agree on
// the cutoffs. It imports only the stdlib — never an app module, never a
// transport library.
package inventory
import "encoding/json"
// Level is the coarse, bus-safe stock state. Consumers map a Level to a display
// hint / availability flag; they never see (or need) the exact quantity.
type Level string
const (
LevelNone Level = "none" // nothing available to sell
LevelLow Level = "low" // at or below the low watermark, but still > 0
LevelHigh Level = "high" // comfortably above the low watermark
)
// LevelFor classifies an exact on-hand quantity into a Level given the low
// watermark. It is the single source of the cutoffs: the producer computes the
// emitted level with it, and any consumer that needs to reclassify a quantity
// (e.g. after a synchronous exact query) uses the same function so the two can
// never disagree. qty <= 0 → none; 0 < qty <= low → low; qty > low → high.
func LevelFor(qty, lowWatermark int64) Level {
switch {
case qty <= 0:
return LevelNone
case qty <= lowWatermark:
return LevelLow
default:
return LevelHigh
}
}
// InStock reports whether the level means the SKU can be sold (any level other
// than none) — a convenience for consumers that only care about the boolean.
func (l Level) InStock() bool { return l != LevelNone }
// LevelChanged is the payload of an event.InventoryLevelChanged envelope: the
// SKU + location whose level crossed a threshold, and the new level — never a
// quantity. The envelope's Subject duplicates SKU; Meta["country"] carries the
// tenant/country for consumer routing (matching the catalog wire convention).
type LevelChanged struct {
SKU string `json:"sku"`
Location string `json:"location"`
Level Level `json:"level"`
}
// Encode serialises the payload for event.Event.Payload. Kept here so producer
// and consumers share the exact wire shape; any future renaming lands here.
func (c LevelChanged) Encode() ([]byte, error) { return json.Marshal(c) }
// Decode parses an InventoryLevelChanged payload — the consumer side of Encode.
func Decode(raw []byte) (LevelChanged, error) {
var c LevelChanged
err := json.Unmarshal(raw, &c)
return c, err
}
+47
View File
@@ -0,0 +1,47 @@
package inventory
import "testing"
func TestLevelFor(t *testing.T) {
const low = int64(5)
cases := []struct {
qty int64
want Level
}{
{-3, LevelNone},
{0, LevelNone},
{1, LevelLow},
{5, LevelLow}, // at the watermark is still low
{6, LevelHigh},
{1000, LevelHigh},
}
for _, c := range cases {
if got := LevelFor(c.qty, low); got != c.want {
t.Errorf("LevelFor(%d, %d) = %q, want %q", c.qty, low, got, c.want)
}
}
}
func TestLevelInStock(t *testing.T) {
if LevelNone.InStock() {
t.Error("none should not be in stock")
}
if !LevelLow.InStock() || !LevelHigh.InStock() {
t.Error("low and high should be in stock")
}
}
func TestLevelChangedRoundTrip(t *testing.T) {
in := LevelChanged{SKU: "ABC-1", Location: "se", Level: LevelLow}
raw, err := in.Encode()
if err != nil {
t.Fatalf("encode: %v", err)
}
out, err := Decode(raw)
if err != nil {
t.Fatalf("decode: %v", err)
}
if out != in {
t.Errorf("round-trip mismatch: got %+v, want %+v", out, in)
}
}