hsa-app/internal/storage/lookup.go
Jean-Michel Tremblay 8b7252dad4 Initial commit: HSA receipt tracker
Go app for capturing and archiving HSA-eligible receipts: OIDC/PKCE auth
against Authelia, SQLite storage with dual-write (filesystem + DB blob),
mobile-first upload, and DB export.

Adds AI receipt classification: a config.json catalog of people and
categories (seeded into the DB on startup), a prompt builder that derives
name-order/initial variants from the data (with same-surname ambiguity
handling), and an Anthropic tool-use client behind POST /classify. Tests
run against a mock endpoint; a live integration test is env-gated to the
cheapest model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:40:12 -04:00

70 lines
2.2 KiB
Go

package storage
import (
"fmt"
"strings"
)
// ListCategories returns all categories ordered by label.
func (s *Store) ListCategories() ([]Lookup, error) { return s.listLookup("categories") }
// ListPeople returns all people ("Who") ordered by label.
func (s *Store) ListPeople() ([]Lookup, error) { return s.listLookup("people") }
// AddCategory inserts a new category label and returns its id.
func (s *Store) AddCategory(label string) (int64, error) { return s.addLookup("categories", label) }
// AddPerson inserts a new person label and returns its id.
func (s *Store) AddPerson(label string) (int64, error) { return s.addLookup("people", label) }
// RenameCategory updates a category's label (the change propagates to every
// receipt referencing it, since receipts hold the id, not the text).
func (s *Store) RenameCategory(id int64, label string) error {
return s.renameLookup("categories", id, label)
}
// RenamePerson updates a person's label.
func (s *Store) RenamePerson(id int64, label string) error {
return s.renameLookup("people", id, label)
}
func (s *Store) listLookup(table string) ([]Lookup, error) {
rows, err := s.db.Query(`SELECT id, label FROM ` + table + ` ORDER BY label COLLATE NOCASE`)
if err != nil {
return nil, fmt.Errorf("list %s: %w", table, err)
}
defer rows.Close()
var out []Lookup
for rows.Next() {
var l Lookup
if err := rows.Scan(&l.ID, &l.Label); err != nil {
return nil, fmt.Errorf("scan %s: %w", table, err)
}
out = append(out, l)
}
return out, rows.Err()
}
func (s *Store) addLookup(table, label string) (int64, error) {
label = strings.TrimSpace(label)
if label == "" {
return 0, fmt.Errorf("label cannot be empty")
}
res, err := s.db.Exec(`INSERT INTO `+table+`(label) VALUES (?)`, label)
if err != nil {
return 0, fmt.Errorf("add %s: %w", table, err)
}
return res.LastInsertId()
}
func (s *Store) renameLookup(table string, id int64, label string) error {
label = strings.TrimSpace(label)
if label == "" {
return fmt.Errorf("label cannot be empty")
}
if _, err := s.db.Exec(`UPDATE `+table+` SET label = ? WHERE id = ?`, label, id); err != nil {
return fmt.Errorf("rename %s: %w", table, err)
}
return nil
}