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

118 lines
6.1 KiB
Go

package storage
import (
"strings"
"time"
"gardomatic.kleiax.de/internal/platform/validate"
)
// TaskTriggerType identifies how a species task template calculates its due window.
type TaskTriggerType string
// TaskDurationUnit is the calendar unit used for offsets and durations.
type TaskDurationUnit string
// TaskTemplateOrigin identifies whether a template was entered manually or derived from species data.
type TaskTemplateOrigin string
// Supported task duration units.
const (
TaskDurationDay TaskDurationUnit = "day"
TaskDurationWeek TaskDurationUnit = "week"
TaskDurationMonth TaskDurationUnit = "month"
)
// Supported task trigger types.
const (
// TaskTriggerMonthOfYear repeats within a calendar-year date window.
TaskTriggerMonthOfYear TaskTriggerType = "month_of_year"
// TaskTriggerRelativeToPlanting is offset from the planting date.
TaskTriggerRelativeToPlanting TaskTriggerType = "relative_to_planting"
// TaskTriggerRelativeToLastTask is offset from the last completion.
TaskTriggerRelativeToLastTask TaskTriggerType = "relative_to_last_task"
TaskTriggerRelativeToSowing TaskTriggerType = "relative_to_sowing"
TaskTriggerRelativeToHarvest TaskTriggerType = "relative_to_harvest"
TaskTriggerRelativeToSpeciesPlanting TaskTriggerType = "relative_to_species_planting"
)
// Supported template origins.
const (
TaskTemplateOriginManual TaskTemplateOrigin = "manual"
TaskTemplateOriginSeasonSowing TaskTemplateOrigin = "season_sowing"
TaskTemplateOriginSeasonPlanting TaskTemplateOrigin = "season_planting"
TaskTemplateOriginSeasonHarvest TaskTemplateOrigin = "season_harvest"
)
// SpeciesTaskTemplateModelInterface persists recurring rules for species tasks.
type SpeciesTaskTemplateModelInterface interface {
Insert(template SpeciesTaskTemplate) (SpeciesTaskTemplate, error)
Get(gardenID, id int) (SpeciesTaskTemplate, error)
GetAllForSpecies(gardenID, speciesID int) ([]SpeciesTaskTemplate, error)
Update(gardenID int, template SpeciesTaskTemplate) (SpeciesTaskTemplate, error)
Delete(gardenID, id int) error
}
// ValidateSpeciesTaskTemplate applies scheduling and recurrence rules to a
// species task template.
func ValidateSpeciesTaskTemplate(v *validate.Validator, template SpeciesTaskTemplate) {
v.Check(strings.TrimSpace(template.Title) != "", "title", "must be provided")
v.Check(len(template.Title) <= 500, "title", "must not be more than 500 bytes long")
v.Check(len(template.Description) <= 10_000, "description", "must not be more than 10000 bytes long")
v.Check(template.Priority >= -100 && template.Priority <= 100, "priority", "must be between -100 and 100")
v.Check(template.TriggerType == TaskTriggerMonthOfYear || template.TriggerType == TaskTriggerRelativeToPlanting || template.TriggerType == TaskTriggerRelativeToLastTask || template.TriggerType == TaskTriggerRelativeToSowing || template.TriggerType == TaskTriggerRelativeToHarvest || template.TriggerType == TaskTriggerRelativeToSpeciesPlanting, "trigger_type", "is invalid")
v.Check(template.Origin == TaskTemplateOriginManual || template.Origin == TaskTemplateOriginSeasonSowing || template.Origin == TaskTemplateOriginSeasonPlanting || template.Origin == TaskTemplateOriginSeasonHarvest, "origin", "is invalid")
if template.TriggerType == TaskTriggerMonthOfYear {
v.Check(template.MonthFrom != nil && *template.MonthFrom >= 1 && *template.MonthFrom <= 12, "month_from", "must be between 1 and 12")
validateTemplateDay(v, "day_from", template.DayFrom)
v.Check(template.DayFrom != nil, "day_from", "must be provided")
} else {
v.Check(template.TriggerOffset >= 0, "trigger_offset", "must not be negative")
}
v.Check(template.Duration >= 0, "duration", "must not be negative")
v.Check(validTaskDurationUnit(template.DurationUnit), "duration_unit", "is invalid")
v.Check(validTaskDurationUnit(template.TriggerOffsetUnit), "trigger_offset_unit", "is invalid")
v.Check(template.Recurrence == TaskRecurrenceNone || template.Recurrence == TaskRecurrenceDaily || template.Recurrence == TaskRecurrenceWeekly || template.Recurrence == TaskRecurrenceMonthly || template.Recurrence == TaskRecurrenceYearly, "recurrence", "is invalid")
if template.Recurrence != TaskRecurrenceNone {
v.Check(template.RecurrenceInterval > 0, "recurrence_interval", "must be greater than zero")
}
}
func validTaskDurationUnit(unit TaskDurationUnit) bool {
return unit == TaskDurationDay || unit == TaskDurationWeek || unit == TaskDurationMonth
}
func validateTemplateDay(v *validate.Validator, field string, value *int) {
if value != nil {
v.Check(*value >= 1 && *value <= 31, field, "must be between 1 and 31")
}
}
// SpeciesTaskTemplate defines a recurring task rule for a species.
type SpeciesTaskTemplate struct {
ID int `json:"id"`
SpeciesID int `json:"species_id"`
Origin TaskTemplateOrigin `json:"origin"`
Title string `json:"title"`
Description string `json:"description"`
TriggerType TaskTriggerType `json:"trigger_type"`
MonthFrom *int `json:"month_from,omitempty"`
DayFrom *int `json:"day_from,omitempty"`
MonthTo *int `json:"month_to,omitempty"`
DayTo *int `json:"day_to,omitempty"`
OffsetDaysFrom *int `json:"offset_days_from,omitempty"`
OffsetDaysTo *int `json:"offset_days_to,omitempty"`
IntervalDays *int `json:"interval_days,omitempty"`
TriggerOffset int `json:"trigger_offset"`
TriggerOffsetUnit TaskDurationUnit `json:"trigger_offset_unit"`
Duration int `json:"duration"`
DurationUnit TaskDurationUnit `json:"duration_unit"`
Recurrence TaskRecurrence `json:"recurrence,omitempty"`
RecurrenceInterval int `json:"recurrence_interval"`
Priority int `json:"priority"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Version int `json:"version"`
}