76 lines
1.9 KiB
Go
76 lines
1.9 KiB
Go
|
|
// 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
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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
|
||
|
|
}
|