Build image / build-and-push (push) Successful in 8s
Co-authored-by: Steve Dudenhoeffer <[email protected]>
167 lines
5.2 KiB
Go
167 lines
5.2 KiB
Go
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)
|
|
}
|