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