package models import ( "database/sql/driver" "encoding/json" "fmt" "strconv" "strings" ) // FlexibleInt is an integer column that accepts what clients actually send. // // The counterpart to FlexibleString, and it exists for the same reason: a strict // type here does not reject one field, it rejects the WHOLE request. Fiber's // BodyParser fails on the first field it cannot convert, so `"status": "Active"` // or `"primaryaddress": true` returns 400 "Invalid request body" and nothing is // saved. On the address form that surfaced as a shopper adding a delivery // address, seeing no error worth acting on, and then finding no address to pick // at checkout — measured 2026-09-02: three plausible payloads, three 400s. // // Accepted, in the shapes a form or a phone actually produces: // // 1 a number // "1" a number as a string, which is what a text input gives // true a checkbox — "set as my default address" // "" an untouched field, read as 0 rather than an error // "Active" a word, mapped by meaning: active/yes/true/default are 1 // // A word it does not recognise is an error, not a silent 0. Guessing there // would store something the shopper did not choose. type FlexibleInt int func (fi *FlexibleInt) UnmarshalJSON(b []byte) error { if len(b) == 0 || string(b) == "null" { return nil } if b[0] == '"' { var s string if err := json.Unmarshal(b, &s); err != nil { return err } s = strings.TrimSpace(s) if s == "" { *fi = 0 return nil } if n, err := strconv.Atoi(s); err == nil { *fi = FlexibleInt(n) return nil } switch strings.ToLower(s) { case "active", "yes", "true", "default", "primary": *fi = 1 case "inactive", "no", "false": *fi = 0 default: return fmt.Errorf("cannot read %q as a number", s) } return nil } if b[0] == 't' || b[0] == 'f' { var v bool if err := json.Unmarshal(b, &v); err != nil { return err } if v { *fi = 1 } else { *fi = 0 } return nil } // A number, possibly written with a decimal point by a client that has no // integer type of its own. var f float64 if err := json.Unmarshal(b, &f); err != nil { return err } *fi = FlexibleInt(int(f)) return nil } func (fi FlexibleInt) MarshalJSON() ([]byte, error) { return json.Marshal(int(fi)) } // Scan reads the column back. bigint arrives as int64; the string cases cover a // text column that holds a number, which this schema has in places. func (fi *FlexibleInt) Scan(value interface{}) error { switch v := value.(type) { case nil: *fi = 0 case int64: *fi = FlexibleInt(v) case int: *fi = FlexibleInt(v) case float64: *fi = FlexibleInt(int(v)) case []byte: n, _ := strconv.Atoi(strings.TrimSpace(string(v))) *fi = FlexibleInt(n) case string: n, _ := strconv.Atoi(strings.TrimSpace(v)) *fi = FlexibleInt(n) default: return fmt.Errorf("cannot read %T as a number", value) } return nil } func (fi FlexibleInt) Value() (driver.Value, error) { return int64(fi), nil }