package api import ( "encoding/json" "errors" "net/http" "strconv" "github.com/gin-gonic/gin" "gitea.stevedudenhoeffer.com/steve/pansy/internal/domain" "gitea.stevedudenhoeffer.com/steve/pansy/internal/service" ) // Seed lots (#50): what you actually bought, and what's left of it. Private to // the buyer — a lot is never shared along with a garden — so every handler here // scopes to the session actor with no garden in the picture. // seedLotCreateRequest is the body for POST /seed-lots. type seedLotCreateRequest struct { PlantID int64 `json:"plantId" binding:"required"` Vendor string `json:"vendor"` SourceURL string `json:"sourceUrl"` SKU string `json:"sku"` LotCode string `json:"lotCode"` PurchasedAt *string `json:"purchasedAt"` PackedForYear *int `json:"packedForYear"` Quantity float64 `json:"quantity"` Unit string `json:"unit" binding:"required"` CostCents *int `json:"costCents"` GerminationPct *float64 `json:"germinationPct"` Notes string `json:"notes"` } func (r seedLotCreateRequest) toInput() service.SeedLotInput { return service.SeedLotInput{ PlantID: r.PlantID, Vendor: r.Vendor, SourceURL: r.SourceURL, SKU: r.SKU, LotCode: r.LotCode, PurchasedAt: r.PurchasedAt, PackedForYear: r.PackedForYear, Quantity: r.Quantity, Unit: r.Unit, CostCents: r.CostCents, GerminationPct: r.GerminationPct, Notes: r.Notes, } } // seedLotUpdateRequest is the body for PATCH /seed-lots/:id: every field // optional, plus the required current version. The nullable columns are // json.RawMessage so an explicit null (clear it) is distinguishable from an // absent field (leave it alone). plantId is not accepted — see SeedLotPatch. type seedLotUpdateRequest struct { Vendor *string `json:"vendor"` SourceURL *string `json:"sourceUrl"` SKU *string `json:"sku"` LotCode *string `json:"lotCode"` PurchasedAt json.RawMessage `json:"purchasedAt"` PackedForYear json.RawMessage `json:"packedForYear"` Quantity *float64 `json:"quantity"` Unit *string `json:"unit"` CostCents json.RawMessage `json:"costCents"` GerminationPct json.RawMessage `json:"germinationPct"` Notes *string `json:"notes"` Version int64 `json:"version" binding:"required"` } func (r seedLotUpdateRequest) toPatch() (service.SeedLotPatch, error) { p := service.SeedLotPatch{ Vendor: r.Vendor, SourceURL: r.SourceURL, SKU: r.SKU, LotCode: r.LotCode, Quantity: r.Quantity, Unit: r.Unit, Notes: r.Notes, } var err error if p.PurchasedAt, p.SetPurchasedAt, err = parseNullable[string](r.PurchasedAt); err != nil { return p, err } if p.PackedForYear, p.SetPackedForYear, err = parseNullable[int](r.PackedForYear); err != nil { return p, err } if p.CostCents, p.SetCostCents, err = parseNullable[int](r.CostCents); err != nil { return p, err } if p.GerminationPct, p.SetGerminationPct, err = parseNullable[float64](r.GerminationPct); err != nil { return p, err } return p, nil } func (h *handlers) listSeedLots(c *gin.Context) { var plantID *int64 if raw := c.Query("plantId"); raw != "" { id, err := strconv.ParseInt(raw, 10, 64) if err != nil || id < 1 { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "invalid plantId") return } plantID = &id } lots, err := h.svc.ListSeedLots(c.Request.Context(), mustActor(c).ID, plantID) if err != nil { writeServiceError(c, err) return } c.JSON(http.StatusOK, lots) } func (h *handlers) createSeedLot(c *gin.Context) { var req seedLotCreateRequest if err := c.ShouldBindJSON(&req); err != nil { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "invalid seed lot: plantId and unit are required") return } lot, err := h.svc.CreateSeedLot(c.Request.Context(), mustActor(c).ID, req.toInput()) if err != nil { writeServiceError(c, err) return } c.JSON(http.StatusCreated, lot) } func (h *handlers) getSeedLot(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } lot, err := h.svc.GetSeedLot(c.Request.Context(), mustActor(c).ID, id) if err != nil { writeServiceError(c, err) return } c.JSON(http.StatusOK, lot) } func (h *handlers) updateSeedLot(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } var req seedLotUpdateRequest if err := c.ShouldBindJSON(&req); err != nil { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "invalid update: a current version is required") return } patch, err := req.toPatch() if err != nil { writeAPIError(c, http.StatusBadRequest, "INVALID_INPUT", "invalid update payload") return } lot, err := h.svc.UpdateSeedLot(c.Request.Context(), mustActor(c).ID, id, patch, req.Version) if err != nil { if errors.Is(err, domain.ErrVersionConflict) { writeVersionConflict(c, lot) return } writeServiceError(c, err) return } c.JSON(http.StatusOK, lot) } func (h *handlers) deleteSeedLot(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } if err := h.svc.DeleteSeedLot(c.Request.Context(), mustActor(c).ID, id); err != nil { writeServiceError(c, err) return } c.Status(http.StatusNoContent) }