package config // This file loads the editable, non-secret lists from config.json (path set by // CONFIG_PATH) into Go structs: the people a receipt can belong to, and the // categories it can fall into. It is separate from config.go, which only reads // environment variables (secrets, ports, paths). On startup these lists are // seeded into the database; the per-category "examples" are used to build the // receipt-classification prompt and deliberately do NOT live in the DB, so they // can be tuned without a schema change. import ( "encoding/json" "fmt" "os" "strings" ) // Catalog is the parsed content of config.json. type Catalog struct { Persons []Person `json:"persons"` Categories []Category `json:"categories"` } // Person is one possible "Who" for a receipt, stored structurally (last/first/ // middle) so the classifier can derive the many ways a name may appear on a // receipt — different order, a comma, or initials. type Person struct { Last string `json:"last"` First string `json:"first"` Middle string `json:"middle"` } // Category is one classification bucket plus authored examples (store names, // typical line items) that help the classifier decide. Examples are semantic and // cannot be derived from the name, so they are authored here by hand. type Category struct { Name string `json:"name"` Examples []string `json:"examples"` } // Label is the canonical display name ("First Last"), used both as the DB label // and as the exact string the classifier must emit. The middle name is carried // only to disambiguate; it is not part of the canonical label. func (p Person) Label() string { return strings.TrimSpace(strings.TrimSpace(p.First) + " " + strings.TrimSpace(p.Last)) } // LoadCatalog reads and validates config.json at path. A missing file is an error: // the app needs people and categories to function. func LoadCatalog(path string) (Catalog, error) { f, err := os.Open(path) if err != nil { return Catalog{}, fmt.Errorf("read config file %q: %w", path, err) } defer f.Close() var c Catalog dec := json.NewDecoder(f) dec.DisallowUnknownFields() if err := dec.Decode(&c); err != nil { return Catalog{}, fmt.Errorf("parse config file %q: %w", path, err) } for i, p := range c.Persons { if strings.TrimSpace(p.First) == "" || strings.TrimSpace(p.Last) == "" { return Catalog{}, fmt.Errorf("person %d: first and last are required", i) } } for i, cat := range c.Categories { if strings.TrimSpace(cat.Name) == "" { return Catalog{}, fmt.Errorf("category %d: name is required", i) } } return c, nil } // CategoryNames returns the category labels in config order. func (c Catalog) CategoryNames() []string { out := make([]string, 0, len(c.Categories)) for _, cat := range c.Categories { out = append(out, cat.Name) } return out } // PersonLabels returns the canonical person labels in config order. func (c Catalog) PersonLabels() []string { out := make([]string, 0, len(c.Persons)) for _, p := range c.Persons { out = append(out, p.Label()) } return out }