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) + } + }) + } +}