package models import ( "errors" "strings" "time" ) const ( RoleReader = "Reader" RoleAdmin = "Admin" ) var AbuseCategories = []string{ "physical", "psychological", "sexual", "economic", "material", "digital", "stalking", "threats", "honor_related", } var Genders = []string{"female", "male", "non_binary", "unknown"} var Statuses = []string{"new", "open", "referred", "closed"} var FaroeLocations = []string{ "Akrar", "Argir", "Ánirnar", "Árnafjørður", "Bøur", "Dalur", "Depil", "Eiði", "Elduvík", "Fámjin", "Froðba", "Fuglafjørður", "Funningsfjørður", "Funningur", "Gásadalur", "Gjógv", "Glyvrar", "Gøtugjógv", "Haldórsvík", "Haraldssund", "Hattarvík", "Hellurnar", "Hestur", "Hov", "Hoyvík", "Hósvík", "Húsar", "Húsavík", "Hvalba", "Hvalvík", "Hvannasund", "Hvítanes", "Innan Glyvur", "Kaldbak", "Kaldbaksbotnur", "Kirkja", "Kirkjubøur", "Klaksvík", "Kolbeinagjógv", "Kollafjørður", "Koltur", "Kunoy", "Kvívík", "Lamba", "Lambareiði", "Langasandur", "Leirvík", "Leynar", "Ljósá", "Lopra", "Miðvágur", "Mikladalur", "Mjørkadalur", "Morskranes", "Múli", "Mykines", "Nes", "Nesvík", "Nólsoy", "Norðdepil", "Norðoyri", "Norðradalur", "Norðragøta", "Norðskáli", "Norðtoftir", "Oyndarfjørður", "Oyrarbakki", "Oyrareingir", "Oyri", "Porkeri", "Rituvík", "Runavík", "Saksun", "Saltangará", "Saltnes", "Sandavágur", "Sandur", "Sandvík", "Selatrað", "Signabøur", "Skarvanes", "Skála", "Skálafjørður/Skálabotnur", "Skálafjørður (Eysturkommuna)", "Skálavík", "Skipanes", "Skopun", "Skúgvoy", "Skælingur", "Stóra Dímun", "Strendur", "Streymnes", "Stykkið", "Sumba", "Sund", "Svínáir", "Svínoy", "Syðradalur (Kalsoy)", "Syðradalur (Streymoy)", "Syðrugøta", "Søldarfjørður", "Sørvágur", "Tjørnuvík", "Toftir", "Tórshavn", "Trongisvágur", "Trøllanes", "Tvøroyri", "Undir Gøtueiði", "Vatnsoyrar", "Vágur", "Válur", "Velbastaður", "Vestmanna", "Viðareiði", "Víkarbyrgi", "Æðuvík", "Ørðavík/Øravík", "Øravíkarlíð", } type Registration struct { ID string `json:"id"` RegisteredAt string `json:"registered_at"` Gender string `json:"gender"` Location string `json:"location"` AbuseType string `json:"abuse_type"` Status string `json:"status"` } type RegistrationFilter struct { Location string AbuseType string Gender string Status string Search string From time.Time To time.Time HasFrom bool HasTo bool Limit int Offset int } func NormalizeRegistration(input *Registration) error { registeredAt := strings.TrimSpace(input.RegisteredAt) if registeredAt == "" { input.RegisteredAt = time.Now().UTC().Format(time.RFC3339) } else { parsed, err := ParseInputTime(registeredAt) if err != nil { return errors.New("registered_at must be RFC3339 or YYYY-MM-DD") } input.RegisteredAt = parsed.UTC().Format(time.RFC3339) } gender := strings.ToLower(strings.TrimSpace(input.Gender)) if gender == "" { gender = "unknown" } if !ContainsFold(Genders, gender) { return errors.New("gender must be one of: female, male, non_binary, unknown") } input.Gender = gender location, ok := CanonicalLocation(input.Location) if !ok { return errors.New("location must be one of the allowed Faroese towns/villages") } input.Location = location abuseType := strings.ToLower(strings.TrimSpace(input.AbuseType)) if !ContainsFold(AbuseCategories, abuseType) { return errors.New("abuse_type must be one of the allowed categories") } input.AbuseType = abuseType status := strings.ToLower(strings.TrimSpace(input.Status)) if status == "" { status = "new" } if !ContainsFold(Statuses, status) { return errors.New("status must be one of: new, open, referred, closed") } input.Status = status return nil } func ParseInputTime(value string) (time.Time, error) { value = strings.TrimSpace(value) if parsed, err := time.Parse(time.RFC3339, value); err == nil { return parsed, nil } if parsed, err := time.Parse("2006-01-02", value); err == nil { return parsed, nil } return time.Time{}, errors.New("invalid time") } func CanonicalLocation(value string) (string, bool) { needle := strings.ToLower(strings.TrimSpace(value)) for _, location := range FaroeLocations { if strings.ToLower(location) == needle { return location, true } } return "", false } func ContainsFold(values []string, needle string) bool { needle = strings.ToLower(strings.TrimSpace(needle)) for _, value := range values { if strings.ToLower(strings.TrimSpace(value)) == needle { return true } } return false }