Files
commerce/api/inventory/handlers.go
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

96 lines
3.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package inventory
import (
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/middleware"
inventoryModel "github.com/hanzoai/commerce/models/inventory"
"github.com/hanzoai/commerce/models/inventorylevel"
"github.com/hanzoai/commerce/models/reservation"
"github.com/hanzoai/commerce/models/variantinventorylink"
"github.com/hanzoai/commerce/util/json"
"github.com/hanzoai/commerce/util/json/http"
"github.com/hanzoai/commerce/util/rest"
)
func Route(router *zip.Group, args ...zip.Handler) {
namespaced := middleware.Namespace()
// Inventory Items - standard CRUD
rest.New(inventoryModel.InventoryItem{}).Route(router, args...)
// Inventory Levels with stock adjustment endpoint
levelApi := rest.New(inventorylevel.InventoryLevel{})
levelApi.POST("/:inventorylevelid/adjust", namespaced, AdjustStock)
levelApi.Route(router, args...)
// Reservations
rest.New(reservation.ReservationItem{}).Route(router, args...)
// Variant-Inventory Links
rest.New(variantinventorylink.VariantInventoryLink{}).Route(router, args...)
}
// adjustRequest represents a stock adjustment request body.
type adjustRequest struct {
StockedQuantity *int `json:"stockedQuantity"`
ReservedQuantity *int `json:"reservedQuantity"`
IncomingQuantity *int `json:"incomingQuantity"`
}
// AdjustStock adjusts StockedQuantity, ReservedQuantity, and/or IncomingQuantity
// on an InventoryLevel by the delta values provided in the request body.
func AdjustStock(c *zip.Ctx) error {
ctx := c.Context()
db := datastore.New(ctx)
id := c.Param("inventorylevelid")
// Load existing inventory level
level := inventorylevel.New(db)
if err := level.GetById(id); err != nil {
return http.Fail(c, 404, "Inventory level not found", err)
}
// Parse adjustment
var adj adjustRequest
if err := json.DecodeBytes(c.Body(), &adj); err != nil {
return http.Fail(c, 400, "Failed to decode request body", err)
}
// Compute the post-adjustment values WITHOUT mutating yet, so a rejected
// oversell leaves the stored level untouched.
stocked := level.StockedQuantity
reserved := level.ReservedQuantity
incoming := level.IncomingQuantity
if adj.StockedQuantity != nil {
stocked += *adj.StockedQuantity
}
if adj.ReservedQuantity != nil {
reserved += *adj.ReservedQuantity
}
if adj.IncomingQuantity != nil {
incoming += *adj.IncomingQuantity
}
// Oversell guard: available (stocked − reserved) must never go negative.
// Reserving more than is on hand — or stocking down below what is already
// reserved — would sell inventory that does not exist. Refuse and DO NOT
// persist. Incoming/restock (positive stocked) and releasing a reservation
// (negative reserved) stay allowed because they keep available ≥ 0.
if stocked-reserved < 0 {
return http.Fail(c, 409, "insufficient available stock", nil)
}
level.StockedQuantity = stocked
level.ReservedQuantity = reserved
level.IncomingQuantity = incoming
// Persist
if err := level.Update(); err != nil {
return http.Fail(c, 500, "Failed to adjust inventory level", err)
}
return http.Render(c, 200, level)
}