Files
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

89 lines
4.2 KiB
Go

package storage
import (
"strings"
"time"
"gardomatic.kleiax.de/internal/platform/validate"
)
// TaskRecurrence identifies the calendar interval of a manual task.
type TaskRecurrence string
// Supported task recurrence values. An empty value disables recurrence.
const (
TaskRecurrenceNone TaskRecurrence = ""
TaskRecurrenceDaily TaskRecurrence = "daily"
TaskRecurrenceWeekly TaskRecurrence = "weekly"
TaskRecurrenceMonthly TaskRecurrence = "monthly"
TaskRecurrenceYearly TaskRecurrence = "yearly"
)
// TaskModelInterface persists garden work items and generated task instances.
type TaskModelInterface interface {
Insert(task Task) (Task, error)
Get(gardenID, id int) (Task, error)
GetAllForGarden(gardenID int) ([]Task, error)
Update(gardenID int, task Task) (Task, error)
Delete(gardenID, id int) error
}
// ValidateTask applies persistence-independent rules for manual garden tasks.
func ValidateTask(v *validate.Validator, task Task) {
v.Check(strings.TrimSpace(task.Title) != "", "title", "must be provided")
v.Check(len(task.Title) <= 500, "title", "must not be more than 500 bytes long")
v.Check(len(task.Description) <= 10_000, "description", "must not be more than 10000 bytes long")
v.Check(task.Priority >= -100 && task.Priority <= 100, "priority", "must be between -100 and 100")
if task.PlantID != nil {
v.Check(*task.PlantID > 0, "plant_id", "must be a positive integer")
}
if task.LocationID != nil {
v.Check(*task.LocationID > 0, "location_id", "must be a positive integer")
}
if task.DueAtStart != nil && task.DueAtEnd != nil {
v.Check(!task.DueAtStart.After(*task.DueAtEnd), "due_at_end", "must not be before due_at_start")
}
v.Check(task.Recurrence == TaskRecurrenceNone || task.Recurrence == TaskRecurrenceDaily || task.Recurrence == TaskRecurrenceWeekly || task.Recurrence == TaskRecurrenceMonthly || task.Recurrence == TaskRecurrenceYearly, "recurrence", "is invalid")
if task.Recurrence != TaskRecurrenceNone {
v.Check(task.DueAtStart != nil || task.DueAtEnd != nil, "recurrence", "requires a due date")
v.Check(task.RecurrenceInterval > 0, "recurrence_interval", "must be greater than zero")
}
validateOptionalEnum(v, "plant_status_on_completion", task.PlantStatusOnCompletion, "alive", "dead", "removed", "infested", "harvested")
if task.PlantStatusOnCompletion != nil {
v.Check(task.PlantID != nil, "plant_status_on_completion", "requires a plant")
}
if task.CompletedAt != nil {
v.Check(task.CompletedBy != nil && *task.CompletedBy > 0, "completed_by", "must be set for a completed task")
}
if task.CompletedBy != nil {
v.Check(task.CompletedAt != nil, "completed_at", "must be set when completed_by is set")
}
}
// Task represents a manual or template-generated garden work item.
type Task struct {
ID int `json:"id"`
GardenID int `json:"garden_id"`
PlantID *int `json:"plant_id,omitempty"`
LocationID *int `json:"location_id,omitempty"`
TemplateID *int `json:"template_id,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
DueAtStart *time.Time `json:"due_at_start,omitempty"`
DueAtEnd *time.Time `json:"due_at_end,omitempty"`
GeneratedFor *time.Time `json:"generated_for,omitempty"`
Recurrence TaskRecurrence `json:"recurrence,omitempty"`
RecurrenceInterval int `json:"recurrence_interval"`
RepeatFromID *int `json:"repeat_from_id,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
CompletedBy *int `json:"completed_by,omitempty"`
Priority int `json:"priority"`
Active bool `json:"active"`
CreatedBy int `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Version int `json:"version"`
Tags []string `json:"tags,omitempty"`
PlantStatusOnCompletion *string `json:"plant_status_on_completion,omitempty"`
}