aboutsummaryrefslogtreecommitdiff
path: root/pkg/localstore/store.go
diff options
context:
space:
mode:
authorleshe4ka46 <alex9102naid1@ya.ru>2025-10-27 20:36:28 +0300
committerleshe4ka46 <alex9102naid1@ya.ru>2025-10-28 13:42:21 +0300
commitbb833561aa74f02970aee13cdc75973b29716491 (patch)
tree0914668e11dbf825979f7419ce1bc78294cd3f7f /pkg/localstore/store.go
parente17a425dfb3382310fb5863f516dacdca9f44956 (diff)
# This is a combination of 2 commits.
# This is the 1st commit message: unmarshal all formats, merge them in the single table, users are truly unique # This is the commit message #2: i
Diffstat (limited to 'pkg/localstore/store.go')
-rw-r--r--pkg/localstore/store.go638
1 files changed, 638 insertions, 0 deletions
diff --git a/pkg/localstore/store.go b/pkg/localstore/store.go
new file mode 100644
index 0000000..151b2b4
--- /dev/null
+++ b/pkg/localstore/store.go
@@ -0,0 +1,638 @@
+package localstore
+
+import (
+ "errors"
+ "strings"
+ "sync"
+ "time"
+ "unicode/utf8"
+
+ "airlines/pkg/model"
+)
+
+type userNameKey struct {
+ Surname string
+ Name string
+ Fathersname string // "" allowed
+ BirthYMD int32 // 0 if unknown
+}
+
+type userInitKey struct {
+ Surname string
+ Name string
+ Init string // one letter
+ BirthYMD int32
+}
+
+type cardKey struct {
+ Prefix string
+ Bonus string // may be ""
+ Number uint64
+}
+
+type cardPairKey struct {
+ Prefix string
+ Number uint64
+}
+
+type flightKey struct {
+ Number string
+ From string
+ To string
+ DateYMD int32
+ HasTime bool
+ Sec int32 // seconds since local midnight if HasTime
+}
+
+type flightDayKey struct {
+ Number string
+ From string
+ To string
+ DateYMD int32
+}
+
+
+type Store struct {
+ mu sync.RWMutex
+
+ users []*model.User
+ cards []*model.Card
+ flights []*model.Flight
+
+ nickToUID map[string]uint64
+ nameToUID map[userNameKey]uint64
+ nameInitToUID map[userInitKey]uint64
+
+ cardToCID map[cardKey]uint64
+ cardPairToCID map[cardPairKey]uint64
+
+ flightToFID map[flightKey]uint64
+ flightByDay map[flightDayKey]uint64
+
+ userFlights map[uint64]map[uint64]struct{} // user_id -> set(flight_id)
+ cardFlights map[uint64]map[uint64]struct{} // card_id -> set(flight_id)
+
+ codesByUser map[uint64]map[string]struct{} // user_id -> set(code)
+ countriesByUser map[uint64]map[string]struct{} // user_id -> set(country)
+ cardsByUser map[uint64]map[uint64]struct{} // user_id -> set(card_id)
+}
+
+func NewLocalStore() *Store {
+ return &Store{
+ users: make([]*model.User, 1),
+ cards: make([]*model.Card, 1),
+ flights: make([]*model.Flight, 1),
+ nickToUID: make(map[string]uint64, 1<<12),
+ nameToUID: make(map[userNameKey]uint64, 1<<12),
+ cardToCID: make(map[cardKey]uint64, 1<<14),
+ cardPairToCID: make(map[cardPairKey]uint64, 1<<14),
+ flightToFID: make(map[flightKey]uint64, 1<<15),
+ flightByDay: make(map[flightDayKey]uint64, 1<<15),
+ userFlights: make(map[uint64]map[uint64]struct{}, 1<<12),
+ cardFlights: make(map[uint64]map[uint64]struct{}, 1<<12),
+ nameInitToUID: make(map[userInitKey]uint64, 1<<12),
+ codesByUser: make(map[uint64]map[string]struct{}, 1<<12),
+ countriesByUser: make(map[uint64]map[string]struct{}, 1<<12),
+ cardsByUser: make(map[uint64]map[uint64]struct{}, 1<<12),
+ }
+}
+
+
+func isZeroCoord(c model.LatLong) bool { return c.Lat == 0 && c.Long == 0 }
+
+func ymdUTC(t time.Time) int32 {
+ if t.IsZero() || t.Year() == model.SentinelBirthday().Year() {
+ return 0
+ }
+ u := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
+ return int32(u.Year()*10000 + int(u.Month())*100 + u.Day())
+}
+
+func secSinceMidnight(t time.Time) int32 {
+ h, m, s := t.Clock()
+ return int32(h*3600 + m*60 + s)
+}
+
+func ensureSet(m map[uint64]map[uint64]struct{}, k uint64) map[uint64]struct{} {
+ s, ok := m[k]
+ if !ok {
+ s = make(map[uint64]struct{}, 8)
+ m[k] = s
+ }
+ return s
+}
+
+/* ============================== users ============================= */
+
+func fatherInitial(s string) string {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return ""
+ }
+ r, _ := utf8.DecodeRuneInString(s)
+ if r == utf8.RuneError {
+ return ""
+ }
+ return string(r) // your pipeline keeps it UPPER
+}
+
+func addUserInitIndex(m map[userInitKey]uint64, u *model.User) {
+ k := userInitKey{Surname: u.Surname, Name: u.Name, Init: fatherInitial(u.Fathersname), BirthYMD: ymdUTC(u.Birthday)}
+ if _, exists := m[k]; !exists {
+ m[k] = u.ID
+ }
+}
+
+func delUserInitIndex(m map[userInitKey]uint64, u *model.User) {
+ k := userInitKey{Surname: u.Surname, Name: u.Name, Init: fatherInitial(u.Fathersname), BirthYMD: ymdUTC(u.Birthday)}
+ delete(m, k)
+}
+
+// --- merge helper (unchanged; keeps initial→full, birthday, nick, sex upgrades) ---
+func (s *Store) mergeUserFields(id uint64, in *model.User) *model.User {
+ ex := s.users[id]
+ // fathersname: initial -> full (same initial), move indexes
+ if ex.Fathersname != "" && len([]rune(ex.Fathersname)) == 1 &&
+ in.Fathersname != "" && fatherInitial(ex.Fathersname) == fatherInitial(in.Fathersname) &&
+ ex.Fathersname != in.Fathersname {
+ oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}
+ if _, ok := s.nameToUID[oldNameKey]; ok {
+ delete(s.nameToUID, oldNameKey)
+ }
+ delUserInitIndex(s.nameInitToUID, ex)
+ ex.Fathersname = in.Fathersname
+ s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id
+ addUserInitIndex(s.nameInitToUID, ex)
+ }
+ // birthday upgrade
+ if ymdUTC(ex.Birthday) == 0 && ymdUTC(in.Birthday) != 0 {
+ oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: 0}
+ if _, ok := s.nameToUID[oldNameKey]; ok {
+ delete(s.nameToUID, oldNameKey)
+ }
+ delUserInitIndex(s.nameInitToUID, ex)
+ ex.Birthday = in.Birthday
+ s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id
+ addUserInitIndex(s.nameInitToUID, ex)
+ }
+ // nick/sex
+ if ex.Nick == "" && in.Nick != "" {
+ ex.Nick = in.Nick
+ s.nickToUID[in.Nick] = id
+ }
+ if ex.Sex == model.SexUnknown && in.Sex != model.SexUnknown {
+ ex.Sex = in.Sex
+ }
+ return ex
+}
+
+// --- FIXED SaveUser: initial-key lookup tries BOTH (birth, 0) ---
+func (s *Store) SaveUser(u *model.User) (*model.User, error) {
+ if u == nil {
+ return nil, errors.New("nil user")
+ }
+ // normalize (names already UPPER)
+ u.Nick = strings.TrimSpace(u.Nick)
+ u.Name = strings.TrimSpace(u.Name)
+ u.Surname = strings.TrimSpace(u.Surname)
+ u.Fathersname = strings.Trim(strings.TrimSpace(u.Fathersname), ".,")
+ if u.Birthday.IsZero() {
+ u.Birthday = model.SentinelBirthday()
+ }
+ inBirth := ymdUTC(u.Birthday)
+ inKey := userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: inBirth}
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ // 1) by Nick
+ if u.Nick != "" {
+ if id, ok := s.nickToUID[u.Nick]; ok {
+ return s.mergeUserFields(id, u), nil
+ }
+ }
+
+ // 2) exact tuple
+ if id, ok := s.nameToUID[inKey]; ok {
+ if u.Nick != "" && s.users[id].Nick == "" {
+ s.users[id].Nick = u.Nick
+ s.nickToUID[u.Nick] = id
+ }
+ return s.mergeUserFields(id, u), nil
+ }
+
+ // 3) initial-based match (try with incoming birth, then with 0)
+ init := fatherInitial(u.Fathersname)
+ tryInits := []userInitKey{
+ {Surname: u.Surname, Name: u.Name, Init: init, BirthYMD: inBirth},
+ }
+ if inBirth != 0 {
+ tryInits = append(tryInits, userInitKey{Surname: u.Surname, Name: u.Name, Init: init, BirthYMD: 0})
+ }
+ for _, ik := range tryInits {
+ if id, ok := s.nameInitToUID[ik]; ok {
+ ex := s.users[id]
+
+ // If ex has initial-only and incoming has full (same initial) → upgrade fathers + move indexes
+ if ex.Fathersname == fatherInitial(ex.Fathersname) &&
+ u.Fathersname != "" &&
+ fatherInitial(u.Fathersname) == fatherInitial(ex.Fathersname) {
+ // move name index
+ oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}
+ delete(s.nameToUID, oldNameKey)
+ // remove old init index (birth may differ)
+ delUserInitIndex(s.nameInitToUID, ex)
+
+ ex.Fathersname = u.Fathersname
+
+ newNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}
+ s.nameToUID[newNameKey] = id
+ addUserInitIndex(s.nameInitToUID, ex)
+ }
+
+ // Upgrade birthday if needed
+ if ymdUTC(ex.Birthday) == 0 && inBirth != 0 {
+ // move name key 0 -> inBirth
+ oldNameKey := userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: 0}
+ if _, ok2 := s.nameToUID[oldNameKey]; ok2 {
+ delete(s.nameToUID, oldNameKey)
+ }
+ // move init index 0 -> inBirth
+ delUserInitIndex(s.nameInitToUID, ex)
+ ex.Birthday = u.Birthday
+ s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: inBirth}] = id
+ addUserInitIndex(s.nameInitToUID, ex)
+ }
+
+ // nick/sex upgrades
+ if u.Nick != "" && ex.Nick == "" {
+ ex.Nick = u.Nick
+ s.nickToUID[u.Nick] = id
+ }
+ if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown {
+ ex.Sex = u.Sex
+ }
+ return ex, nil
+ }
+ }
+
+ // 4) relaxed: fathersname empty, same birth
+ if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: "", BirthYMD: inBirth}]; ok {
+ ex := s.users[id]
+ if ex.Fathersname == "" && u.Fathersname != "" {
+ delete(s.nameToUID, userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: "", BirthYMD: inBirth})
+ ex.Fathersname = u.Fathersname
+ s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: inBirth}] = id
+ addUserInitIndex(s.nameInitToUID, ex)
+ }
+ if u.Nick != "" && ex.Nick == "" {
+ ex.Nick = u.Nick
+ s.nickToUID[u.Nick] = id
+ }
+ if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown {
+ ex.Sex = u.Sex
+ }
+ if ymdUTC(ex.Birthday) == 0 && inBirth != 0 {
+ ex.Birthday = u.Birthday
+ }
+ return ex, nil
+ }
+
+ // 5) same fathersname, no birth
+ if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: 0}]; ok {
+ delete(s.nameToUID, userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: u.Fathersname, BirthYMD: 0})
+ ex := s.users[id]
+ ex.Birthday = u.Birthday
+ s.nameToUID[inKey] = id
+ delUserInitIndex(s.nameInitToUID, ex)
+ addUserInitIndex(s.nameInitToUID, ex)
+ if u.Nick != "" && ex.Nick == "" {
+ ex.Nick = u.Nick
+ s.nickToUID[u.Nick] = id
+ }
+ if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown {
+ ex.Sex = u.Sex
+ }
+ return ex, nil
+ }
+
+ // 6) fully unspecific existing (fathers="", birth=0)
+ if id, ok := s.nameToUID[userNameKey{Surname: u.Surname, Name: u.Name, Fathersname: "", BirthYMD: 0}]; ok {
+ ex := s.users[id]
+ delete(s.nameToUID, userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: "", BirthYMD: 0})
+ if ex.Fathersname == "" && u.Fathersname != "" {
+ ex.Fathersname = u.Fathersname
+ }
+ if ymdUTC(ex.Birthday) == 0 && inBirth != 0 {
+ ex.Birthday = u.Birthday
+ }
+ s.nameToUID[userNameKey{Surname: ex.Surname, Name: ex.Name, Fathersname: ex.Fathersname, BirthYMD: ymdUTC(ex.Birthday)}] = id
+ addUserInitIndex(s.nameInitToUID, ex)
+ if u.Nick != "" && ex.Nick == "" {
+ ex.Nick = u.Nick
+ s.nickToUID[u.Nick] = id
+ }
+ if ex.Sex == model.SexUnknown && u.Sex != model.SexUnknown {
+ ex.Sex = u.Sex
+ }
+ return ex, nil
+ }
+
+ // 7) create
+ u.ID = uint64(len(s.users))
+ s.users = append(s.users, u)
+ if u.Nick != "" {
+ s.nickToUID[u.Nick] = u.ID
+ }
+ s.nameToUID[inKey] = u.ID
+ addUserInitIndex(s.nameInitToUID, u)
+ return u, nil
+}
+
+/* ============================== cards ============================= */
+/*
+Match order:
+ 1) exact (Prefix, Number, Bonus)
+ 2) pair (Prefix, Number) → if stored bonus=="" and incoming bonus!="", upgrade in place (move triple index)
+ 3) else create new
+Never steal UserID: only set if existing has 0 and incoming non-zero.
+*/
+
+func (s *Store) SaveCard(c *model.Card) (*model.Card, error) {
+ if c == nil {
+ return nil, errors.New("nil card")
+ }
+ c.Prefix = strings.TrimSpace(c.Prefix)
+ c.Bonusprogramm = strings.TrimSpace(c.Bonusprogramm)
+ if c.Prefix == "" {
+ return nil, errors.New("invalid card: empty prefix")
+ }
+
+ tri := cardKey{Prefix: c.Prefix, Number: c.Number, Bonus: c.Bonusprogramm}
+ pair := cardPairKey{Prefix: c.Prefix, Number: c.Number}
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ // exact triple
+ if id, ok := s.cardToCID[tri]; ok {
+ ex := s.cards[id]
+ if ex.UserID == 0 && c.UserID != 0 {
+ ex.UserID = c.UserID
+ if s.cardsByUser[ex.UserID] == nil {
+ s.cardsByUser[ex.UserID] = make(map[uint64]struct{}, 1024)
+ }
+ v := s.cardsByUser[ex.UserID]
+ v[ex.ID] = struct{}{}
+ s.cardsByUser[ex.UserID] = v
+ }
+ return ex, nil
+ }
+
+ // by pair
+ if id, ok := s.cardPairToCID[pair]; ok {
+ ex := s.cards[id]
+ // link user once
+ if ex.UserID == 0 && c.UserID != 0 {
+ ex.UserID = c.UserID
+ }
+ if s.cardsByUser[ex.UserID] == nil {
+ s.cardsByUser[ex.UserID] = make(map[uint64]struct{}, 1024)
+ }
+ v := s.cardsByUser[ex.UserID]
+ v[ex.ID] = struct{}{}
+ s.cardsByUser[ex.UserID] = v
+ switch {
+ case ex.Bonusprogramm == "" && c.Bonusprogramm != "":
+ // move triple index from empty -> new bonus
+ oldTri := cardKey{Prefix: ex.Prefix, Number: ex.Number, Bonus: ex.Bonusprogramm}
+ delete(s.cardToCID, oldTri)
+ ex.Bonusprogramm = c.Bonusprogramm
+ newTri := cardKey{Prefix: ex.Prefix, Number: ex.Number, Bonus: ex.Bonusprogramm}
+ s.cardToCID[newTri] = id
+ return ex, nil
+ case ex.Bonusprogramm == "" && c.Bonusprogramm == "":
+ return ex, nil
+ case ex.Bonusprogramm != "" && c.Bonusprogramm == "":
+ return ex, nil
+ case ex.Bonusprogramm != "" && c.Bonusprogramm != "" && ex.Bonusprogramm != c.Bonusprogramm:
+ // different program → create new card record
+ default:
+ return ex, nil
+ }
+ }
+
+ // create
+ c.ID = uint64(len(s.cards))
+ s.cards = append(s.cards, c)
+ s.cardPairToCID[pair] = c.ID
+ s.cardToCID[tri] = c.ID // even if bonus == "", we still index triple
+
+ if s.cardsByUser[c.UserID] == nil {
+ s.cardsByUser[c.UserID] = make(map[uint64]struct{}, 1024)
+ }
+ v := s.cardsByUser[c.UserID]
+ v[c.ID] = struct{}{}
+ s.cardsByUser[c.UserID] = v
+
+ return c, nil
+}
+
+/* ============================== flights =========================== */
+/*
+Identity:
+ - date-only: (Number, From, To, DateYMD, false, 0)
+ - timed : (Number, From, To, DateYMD, true, SecSinceMidnight)
+Upgrade:
+ - if a date-only exists and a timed arrives for the same day, upgrade in place
+Merge:
+ - coords: fill when missing
+ - relations: add (dedup via sets)
+*/
+
+func (s *Store) SaveFlight(f *model.Flight) (*model.Flight, error) {
+ if f == nil {
+ return nil, errors.New("nil flight")
+ }
+ f.Number = strings.TrimSpace(f.Number)
+ f.From = strings.TrimSpace(f.From)
+ f.To = strings.TrimSpace(f.To)
+
+ // normalize day
+ dayUTC := time.Date(f.Date.Year(), f.Date.Month(), f.Date.Day(), 0, 0, 0, 0, time.UTC)
+ ymd := ymdUTC(dayUTC)
+
+ var pKey flightKey
+ if f.HasTime {
+ pKey = flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(f.Date)}
+ } else {
+ f.Date = dayUTC // store as date-only
+ pKey = flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: false, Sec: 0}
+ }
+ dayKey := flightDayKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd}
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ // 1) exact (precise) key
+ if id, ok := s.flightToFID[pKey]; ok {
+ ex := s.flights[id]
+ s.mergeFlightFields(id, ex, f)
+ return ex, nil
+ }
+
+ // 2) same day exists -> maybe upgrade date-only to timed
+ if id, ok := s.flightByDay[dayKey]; ok {
+ ex := s.flights[id]
+ exKey := s.keyOfFlight(ex)
+ if !ex.HasTime && f.HasTime {
+ // move map key to timed
+ delete(s.flightToFID, exKey)
+ ex.HasTime = true
+ // set clock from incoming (keep same calendar date)
+ ex.Date = time.Date(dayUTC.Year(), dayUTC.Month(), dayUTC.Day(),
+ f.Date.Hour(), f.Date.Minute(), f.Date.Second(), f.Date.Nanosecond(), f.Date.Location())
+ s.flightToFID[s.keyOfFlight(ex)] = id
+ // day index already points to best precision
+ }
+ // merge fields/relations
+ s.mergeFlightFields(id, ex, f)
+ return ex, nil
+ }
+
+ // 3) brand new
+ f.ID = uint64(len(s.flights))
+ s.flights = append(s.flights, f)
+ s.flightToFID[pKey] = f.ID
+ s.flightByDay[dayKey] = f.ID
+
+ // if s.countriesByUser[f.UserID] == nil {
+ // s.countriesByUser[f.UserID] = make(map[string]struct{}, 1024)
+ // }
+
+ // v := s.countriesByUser[f.UserID]
+ // dd, _ := airports.LookupIATA(f.From)
+ // v[dd.Country] = struct{}{}
+ // s.countriesByUser[f.UserID] = v
+
+ if f.Code != "" {
+ if s.codesByUser[f.UserID] == nil {
+ s.codesByUser[f.UserID] = make(map[string]struct{}, 1024)
+ }
+ codesByUser := s.codesByUser[f.UserID]
+ codesByUser[f.Code] = struct{}{}
+ s.codesByUser[f.UserID] = codesByUser
+ }
+
+ // relations
+ if f.UserID != 0 {
+ ensureSet(s.userFlights, f.UserID)[f.ID] = struct{}{}
+ }
+ if f.CardID != 0 {
+ ensureSet(s.cardFlights, f.CardID)[f.ID] = struct{}{}
+ }
+ return f, nil
+}
+
+func (s *Store) keyOfFlight(f *model.Flight) flightKey {
+ ymd := ymdUTC(time.Date(f.Date.Year(), f.Date.Month(), f.Date.Day(), 0, 0, 0, 0, time.UTC))
+ if f.HasTime {
+ return flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(f.Date)}
+ }
+ return flightKey{Number: f.Number, From: f.From, To: f.To, DateYMD: ymd, HasTime: false, Sec: 0}
+}
+
+func (s *Store) mergeFlightFields(id uint64, ex, in *model.Flight) {
+ // coords: fill when empty
+ if isZeroCoord(ex.FromCoords) && !isZeroCoord(in.FromCoords) {
+ ex.FromCoords = in.FromCoords
+ }
+ if isZeroCoord(ex.ToCoords) && !isZeroCoord(in.ToCoords) {
+ ex.ToCoords = in.ToCoords
+ }
+ // relations
+ if in.UserID != 0 {
+ ensureSet(s.userFlights, in.UserID)[id] = struct{}{}
+ }
+ if in.CardID != 0 {
+ ensureSet(s.cardFlights, in.CardID)[id] = struct{}{}
+ }
+ if in.Code != "" && ex.Code == "" {
+ ex.Code = in.Code
+
+ // if s.codesByUser[in.UserID] == nil {
+ // s.codesByUser[in.UserID] = make(map[string]struct{}, 1024)
+ // }
+ // codesByUser := s.codesByUser[in.UserID]
+ // codesByUser[in.Code] = struct{}{}
+ // s.codesByUser[in.UserID] = codesByUser
+ }
+}
+
+/* ============================== finders =========================== */
+
+func (s *Store) FindUserByNick(nick string) (*model.User, bool) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ id, ok := s.nickToUID[strings.TrimSpace(nick)]
+ if !ok || id == 0 || int(id) >= len(s.users) {
+ return nil, false
+ }
+ return s.users[id], true
+}
+
+func (s *Store) FindUserByName(name, surname, fathers string, bday time.Time) (*model.User, bool) {
+ key := userNameKey{
+ Surname: strings.TrimSpace(surname),
+ Name: strings.TrimSpace(name),
+ Fathersname: strings.TrimSpace(fathers),
+ BirthYMD: ymdUTC(bday),
+ }
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ id, ok := s.nameToUID[key]
+ if !ok || id == 0 || int(id) >= len(s.users) {
+ return nil, false
+ }
+ return s.users[id], true
+}
+
+func (s *Store) FindCard(prefix string, number uint64, bonus string) (*model.Card, bool) {
+ tri := cardKey{Prefix: strings.TrimSpace(prefix), Number: number, Bonus: strings.TrimSpace(bonus)}
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ if id, ok := s.cardToCID[tri]; ok && id != 0 && int(id) < len(s.cards) {
+ return s.cards[id], true
+ }
+ // fall back to pair if no exact
+ pair := cardPairKey{Prefix: strings.TrimSpace(prefix), Number: number}
+ if id, ok := s.cardPairToCID[pair]; ok && id != 0 && int(id) < len(s.cards) {
+ return s.cards[id], true
+ }
+ return nil, false
+}
+
+func (s *Store) FindFlight(number, from, to string, date time.Time, hasTime bool) (*model.Flight, bool) {
+ number = strings.TrimSpace(number)
+ from = strings.TrimSpace(from)
+ to = strings.TrimSpace(to)
+
+ ymd := ymdUTC(time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC))
+ var k flightKey
+ if hasTime {
+ k = flightKey{Number: number, From: from, To: to, DateYMD: ymd, HasTime: true, Sec: secSinceMidnight(date)}
+ } else {
+ k = flightKey{Number: number, From: from, To: to, DateYMD: ymd, HasTime: false, Sec: 0}
+ }
+
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ if id, ok := s.flightToFID[k]; ok && id != 0 && int(id) < len(s.flights) {
+ return s.flights[id], true
+ }
+ // day-level fallback (returns best precision for the day if exact key absent)
+ if id, ok := s.flightByDay[flightDayKey{Number: number, From: from, To: to, DateYMD: ymd}]; ok && id != 0 && int(id) < len(s.flights) {
+ return s.flights[id], true
+ }
+ return nil, false
+}