diff --git a/pkg/inventory/badge.go b/pkg/inventory/badge.go
new file mode 100644
index 0000000..d039330
--- /dev/null
+++ b/pkg/inventory/badge.go
@@ -0,0 +1,51 @@
+package inventory
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+)
+
+// StockReader abstracts the inventory getter for badge rendering.
+type StockReader interface {
+ GetInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error)
+}
+
+// BadgeHandler returns an http.HandlerFunc that renders an HTML stock badge fragment.
+func BadgeHandler(reader StockReader) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ sku := r.URL.Query().Get("sku")
+ if sku == "" {
+ http.Error(w, "missing sku parameter", http.StatusBadRequest)
+ return
+ }
+
+ locationID := LocationID(r.URL.Query().Get("location_id"))
+ if locationID == "" {
+ locationID = "default"
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "private, max-age=5")
+
+ qty, err := reader.GetInventory(r.Context(), SKU(sku), locationID)
+ if err != nil {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(`Availability unavailable`))
+ return
+ }
+
+ var htmlSnippet string
+ switch {
+ case qty > 10:
+ htmlSnippet = fmt.Sprintf(`✓ In stock`, sku)
+ case qty > 0:
+ htmlSnippet = fmt.Sprintf(`⚡ Only %d left!`, sku, qty)
+ default:
+ htmlSnippet = fmt.Sprintf(`✕ Out of stock`, sku)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(htmlSnippet))
+ }
+}
diff --git a/pkg/inventory/badge_test.go b/pkg/inventory/badge_test.go
new file mode 100644
index 0000000..b5fbad6
--- /dev/null
+++ b/pkg/inventory/badge_test.go
@@ -0,0 +1,94 @@
+package inventory
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+type mockStockReader struct {
+ stock map[string]int64
+ err error
+}
+
+func (m *mockStockReader) GetInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) {
+ if m.err != nil {
+ return 0, m.err
+ }
+ qty, ok := m.stock[string(sku)]
+ if !ok {
+ return 0, errors.New("sku not found")
+ }
+ return qty, nil
+}
+
+func TestBadgeHandler(t *testing.T) {
+ mock := &mockStockReader{
+ stock: map[string]int64{
+ "HIGH": 25,
+ "LOW": 3,
+ "OUT": 0,
+ },
+ }
+
+ handler := BadgeHandler(mock)
+
+ tests := []struct {
+ name string
+ url string
+ expectedCode int
+ expectedSubstr string
+ }{
+ {
+ name: "missing sku",
+ url: "/inventory/badge",
+ expectedCode: http.StatusBadRequest,
+ expectedSubstr: "missing sku parameter",
+ },
+ {
+ name: "high stock",
+ url: "/inventory/badge?sku=HIGH",
+ expectedCode: http.StatusOK,
+ expectedSubstr: "stock-in-stock",
+ },
+ {
+ name: "low stock",
+ url: "/inventory/badge?sku=LOW",
+ expectedCode: http.StatusOK,
+ expectedSubstr: "Only 3 left!",
+ },
+ {
+ name: "out of stock",
+ url: "/inventory/badge?sku=OUT",
+ expectedCode: http.StatusOK,
+ expectedSubstr: "stock-out-of-stock",
+ },
+ {
+ name: "unknown stock / error",
+ url: "/inventory/badge?sku=UNKNOWN",
+ expectedCode: http.StatusOK,
+ expectedSubstr: "stock-unknown",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, tt.url, nil)
+ rr := httptest.NewRecorder()
+
+ handler.ServeHTTP(rr, req)
+
+ if rr.Code != tt.expectedCode {
+ t.Errorf("expected status code %d, got %d", tt.expectedCode, rr.Code)
+ }
+
+ body := rr.Body.String()
+ if !strings.Contains(body, tt.expectedSubstr) {
+ t.Errorf("expected body to contain %q, got %q", tt.expectedSubstr, body)
+ }
+ })
+ }
+}