A planting ("plop") is a circular patch of one plant inside a plantable object: center x/y in the object's local frame (so moving/rotating the bed carries its plants), a radius, and either an explicit plant count or one derived from area ÷ spacing². removed_at soft-removes a plop — that's how "clear bed" and future season history work.
Scope
internal/store/plantings.go + internal/service/plantings.go (conventions from #7; mutations require editor role on the object's garden).
POST /api/v1/objects/:id/plantings: object must be plantable; fields plant_id (must be visible to the actor: built-in or own), x_cm, y_cm (local frame), radius_cm > 0, optional count override, optional label, planted_at (default: today). Loose bounds check: center inside the object's unrotated bounds (radius may overhang).
PATCH /api/v1/plantings/:id (partial + version): move, resize radius, change plant/count/label/dates, set/unset removed_at. DELETE /api/v1/plantings/:id: hard delete (for mistakes; "harvested/removed" flows should set removed_at).
Derived count: when count is NULL, responses include derived_count = max(1, round(π·r² / spacing_cm²)) computed from the plant's spacing — one helper in internal/service, unit-tested (this exact formula also feeds #15's display and #19's FillRegion).
/full (#10) now includes active plantings (removed_at IS NULL) + the referenced plants; verify the wiring.
Service tests: place/move/resize, derived vs override count, non-plantable object rejection, foreign plant rejection, removed_at filtering, version conflict.
Out of scope
Frontend (#15). Bulk region fills (#19). Overlap prevention between plops (allowed by design — interplanting is real).
curl: place a garlic plop in a bed corner (local coords), grow its radius — derived_count rises with r²; set count=12 — override wins; set removed_at — it leaves /full.
Planting into a tree/path → 400/422 with a clear message; planting user B's custom plant as user A → rejected.
go test ./internal/service/... green including the count formula's edge cases (tiny radius → 1).
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md)
## Context
A planting ("plop") is a circular patch of one plant inside a plantable object: center x/y **in the object's local frame** (so moving/rotating the bed carries its plants), a radius, and either an explicit plant count or one derived from area ÷ spacing². `removed_at` soft-removes a plop — that's how "clear bed" and future season history work.
## Scope
- [ ] `internal/store/plantings.go` + `internal/service/plantings.go` (conventions from #7; mutations require editor role on the **object's garden**).
- [ ] `POST /api/v1/objects/:id/plantings`: object must be `plantable`; fields plant_id (must be visible to the actor: built-in or own), x_cm, y_cm (local frame), radius_cm > 0, optional count override, optional label, `planted_at` (default: today). Loose bounds check: center inside the object's unrotated bounds (radius may overhang).
- [ ] `PATCH /api/v1/plantings/:id` (partial + `version`): move, resize radius, change plant/count/label/dates, set/unset `removed_at`. `DELETE /api/v1/plantings/:id`: hard delete (for mistakes; "harvested/removed" flows should set `removed_at`).
- [ ] Derived count: when `count` is NULL, responses include `derived_count = max(1, round(π·r² / spacing_cm²))` computed from the plant's spacing — one helper in `internal/service`, unit-tested (this exact formula also feeds #15's display and #19's FillRegion).
- [ ] `/full` (#10) now includes active plantings (`removed_at IS NULL`) + the referenced plants; verify the wiring.
- [ ] Service tests: place/move/resize, derived vs override count, non-plantable object rejection, foreign plant rejection, removed_at filtering, version conflict.
## Out of scope
- Frontend (#15). Bulk region fills (#19). Overlap prevention between plops (allowed by design — interplanting is real).
## Key files & patterns
- Table + formula: `DESIGN.md` § Domain model (`plantings`); local-frame rules § Geometry.
## Dependencies
Blocked by #10 and #12.
## Acceptance criteria
- curl: place a garlic plop in a bed corner (local coords), grow its radius — `derived_count` rises with r²; set `count=12` — override wins; set `removed_at` — it leaves `/full`.
- Planting into a tree/path → 400/422 with a clear message; planting user B's custom plant as user A → rejected.
- `go test ./internal/service/...` green including the count formula's edge cases (tiny radius → 1).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of epic #20 · Design: DESIGN.md
Context
A planting ("plop") is a circular patch of one plant inside a plantable object: center x/y in the object's local frame (so moving/rotating the bed carries its plants), a radius, and either an explicit plant count or one derived from area ÷ spacing².
removed_atsoft-removes a plop — that's how "clear bed" and future season history work.Scope
internal/store/plantings.go+internal/service/plantings.go(conventions from #7; mutations require editor role on the object's garden).POST /api/v1/objects/:id/plantings: object must beplantable; fields plant_id (must be visible to the actor: built-in or own), x_cm, y_cm (local frame), radius_cm > 0, optional count override, optional label,planted_at(default: today). Loose bounds check: center inside the object's unrotated bounds (radius may overhang).PATCH /api/v1/plantings/:id(partial +version): move, resize radius, change plant/count/label/dates, set/unsetremoved_at.DELETE /api/v1/plantings/:id: hard delete (for mistakes; "harvested/removed" flows should setremoved_at).countis NULL, responses includederived_count = max(1, round(π·r² / spacing_cm²))computed from the plant's spacing — one helper ininternal/service, unit-tested (this exact formula also feeds #15's display and #19's FillRegion)./full(#10) now includes active plantings (removed_at IS NULL) + the referenced plants; verify the wiring.Out of scope
Key files & patterns
DESIGN.md§ Domain model (plantings); local-frame rules § Geometry.Dependencies
Blocked by #10 and #12.
Acceptance criteria
derived_countrises with r²; setcount=12— override wins; setremoved_at— it leaves/full.go test ./internal/service/...green including the count formula's edge cases (tiny radius → 1).