platform
This commit is contained in:
1 parent
e706f7d907
commit
21bcb63440
42 files changed
+5903
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user