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"` }