Three real bugs, two of which would have quietly corrupted inventory numbers. CopyGarden carried seed_lot_id into the copied plantings, so copying a garden double-counted the original lot's usage — the copy is a plan, not a second planting out of the same packet. It also quietly attached a private lot to a garden that may later be shared, contradicting the "lots are never shared with a garden" invariant this feature was built on. The copy now drops the link. RestorePlanting restored a seed_lot_id verbatim from a history snapshot. Live rows get their link nulled by ON DELETE SET NULL when a lot is deleted, but the snapshot still holds the old id, so undoing a planting deletion after retiring its lot would violate the foreign key and abort the whole revert. The revert now drops a dangling link before restoring: the plant really was in the ground, which is the part worth restoring. CreateSeedLot returned a lot with Used/Remaining at their zero values, so a packet you just bought reported "0 remaining" — the exact opposite of the truth. The version-conflict path had the same gap, and that row is what the client rebases on, so a 409 reporting remaining=0 is worse than no number at all. Both fill them now. Remaining can go negative and that is deliberate — it means more was planted than the lot recorded buying, which happens (a miscounted packet, a lot entered after the fact). Clamping to zero would hide the discrepancy at exactly the moment it is worth seeing. Now documented and tested rather than incidental. Performance and tidiness: SeedLotUsage is scoped to the lots being filled instead of scanning every planting the owner has, so reading one lot is one lot's work; ListSeedLotsForOwner gained the LIMIT every other list read has; parseNullable moved out of seed_lots.go into the shared request helpers, since every nullable field in the API needs that three-way absent/null/value distinction; finalizePlant validates against its own maxPlantVendorLen rather than borrowing a constant named for seed lots; seedLotForPlanting became checkSeedLotForPlanting, since it validates rather than fetches; and the duplicate intPtr test helper gave way to the existing ptrInt. Declined: recording seed-lot mutations in the revision history. change_sets are anchored to a garden and lots are user-scoped, deliberately — they belong to you, not to any one garden, so there is no garden to record them against. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -69,6 +70,26 @@ func writeVersionConflict(c *gin.Context, current any) {
|
||||
})
|
||||
}
|
||||
|
||||
// parseNullable decodes any JSON value into (value, present) for a nullable
|
||||
// column: absent → (nil, false); explicit null → (nil, true); anything else →
|
||||
// the decoded value. That three-way distinction is what lets a PATCH tell "clear
|
||||
// this to NULL" apart from "leave it alone", and every nullable field in the API
|
||||
// needs it, so it lives here rather than in whichever file happened to want it
|
||||
// first.
|
||||
func parseNullable[T any](raw json.RawMessage) (*T, bool, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
if string(raw) == "null" {
|
||||
return nil, true, nil
|
||||
}
|
||||
var v T
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &v, true, nil
|
||||
}
|
||||
|
||||
// intQuery reads a non-negative integer query parameter, falling back to def on
|
||||
// an absent or malformed value.
|
||||
func intQuery(c *gin.Context, name string, def int) int {
|
||||
|
||||
Reference in New Issue
Block a user