// Package receipt holds the receipt domain model and pure validation logic. package receipt import ( "fmt" "strings" "time" ) // Receipt is a stored receipt record. Category and Who are referenced by id into // the categories/people lookup tables (PersonID is nil when "Who" is unset). type Receipt struct { ID string UploadedBy string UploadedAt time.Time ReceiptDate time.Time AmountCents int64 CategoryID int64 PersonID *int64 FilePath string ImageData []byte FileSizeBytes int64 OriginalFilename string MimeType string DeletedAt *time.Time } // Attachment is a supplementary file belonging to a receipt (a second page, an // itemized list, an EOB). It carries no amount/date/category/who of its own — it // inherits the parent receipt's identity. Stored dual-write like receipts: bytes // on disk and as a DB blob. type Attachment struct { ID string ReceiptID string UploadedBy string UploadedAt time.Time FilePath string ImageData []byte FileSizeBytes int64 OriginalFilename string MimeType string DeletedAt *time.Time } // ParseAmountCents parses a positive dollar amount into integer cents without // using floating point. Accepts an optional leading "$", surrounding spaces, and // thousands separators. Rejects empty, non-numeric, more than two decimal places, // zero, and negative values. func ParseAmountCents(s string) (int64, error) { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "$") s = strings.ReplaceAll(s, ",", "") s = strings.TrimSpace(s) if s == "" { return 0, fmt.Errorf("empty amount") } if strings.HasPrefix(s, "-") { return 0, fmt.Errorf("amount must be positive") } whole, frac, hasDot := strings.Cut(s, ".") if hasDot && strings.Contains(frac, ".") { return 0, fmt.Errorf("invalid amount %q", s) } if whole == "" && frac == "" { return 0, fmt.Errorf("invalid amount %q", s) } // Normalise the fractional part to exactly two digits. switch len(frac) { case 0: frac = "00" case 1: frac = frac + "0" case 2: // ok default: return 0, fmt.Errorf("amount has more than two decimal places: %q", s) } var cents int64 for _, r := range whole + frac { if r < '0' || r > '9' { return 0, fmt.Errorf("invalid amount %q", s) } cents = cents*10 + int64(r-'0') } if cents <= 0 { return 0, fmt.Errorf("amount must be greater than zero") } return cents, nil }