package store import ( "context" "errors" "strings" "time" "airlines/pkg/model" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/logger" ) type Store struct { DB *gorm.DB } func NewStore(dsn string) (*Store, error) { db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ SkipDefaultTransaction: true, // you can wrap outside for big imports PrepareStmt: true, // statement cache DisableNestedTransaction: true, Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return nil, err } return &Store{DB: db}, nil } func (s *Store) AutoMigrate() error { return s.DB.AutoMigrate(&model.User{}, &model.Card{}, &model.Flight{}) } /* ============================= Users ============================= */ func (s *Store) SaveUser(ctx context.Context, in *model.User) (*model.User, error) { if in == nil { return nil, errors.New("nil user") } in.Nick = strings.TrimSpace(in.Nick) in.Name = strings.TrimSpace(in.Name) in.Surname = strings.TrimSpace(in.Surname) in.Fathersname = strings.TrimSpace(in.Fathersname) if in.Birthday.IsZero() { in.Birthday = model.SentinelBirthday() } in.Birthday = in.Birthday.In(time.UTC) out := &model.User{} err := s.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1) Try by Nick if in.Nick != "" { if err := tx.Where("nick = ?", in.Nick).First(out).Error; err == nil { return nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } } // 2) Fallback by identity tuple q := tx.Where("name = ? AND surname = ?", in.Name, in.Surname) if in.Fathersname != "" { q = q.Where("fathersname = ?", in.Fathersname) } else { q = q.Where("(fathersname IS NULL OR fathersname = '')") } if in.Birthday.Year() != model.SentinelBirthday().Year() { q = q.Where("birthday = ?", in.Birthday) } if err := q.First(out).Error; err == nil { return nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } // 3) Not found → create if err := tx.Create(in).Error; err != nil { return err } *out = *in return nil }) return out, err } /* ============================= Cards ============================= */ /* Card→User is a plain FK (cards.user_id). Keep it. */ func (s *Store) SaveCard(ctx context.Context, in *model.Card) (*model.Card, error) { if in == nil { return nil, errors.New("nil card") } in.Prefix = strings.TrimSpace(in.Prefix) in.Bonusprogramm = strings.TrimSpace(in.Bonusprogramm) if in.Prefix == "" || in.Bonusprogramm == "" { return nil, errors.New("invalid card: empty prefix or bonusprogramm") } out := &model.Card{} err := s.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // find by unique triple if err := tx.Where("prefix = ? AND number = ? AND bonusprogramm = ?", in.Prefix, in.Number, in.Bonusprogramm).First(out).Error; err == nil { // Link to user once if requested and not yet linked if in.UserID != 0 && out.UserID == 0 { if err := tx.Model(out).Update("user_id", in.UserID).Error; err != nil { return err } out.UserID = in.UserID } // refuse stealing if different user already set if in.UserID != 0 && out.UserID != 0 && out.UserID != in.UserID { return errors.New("card already linked to another user") } return nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } // not found → create (includes FK if provided) if err := tx.Create(in).Error; err != nil { return err } *out = *in return nil }) return out, err } /* ============================= Flights ============================= */ /* Find/create by (number, from, to, date). Do NOT write user_id/card_id columns. Use associations to link: - flight.Users ↔ user_flights - flight.Cards ↔ card_flights This avoids “raw IDs” on the flight row and uses the relations instead. */ func (s *Store) SaveFlight(ctx context.Context, in *model.Flight) (*model.Flight, error) { if in == nil { return nil, errors.New("nil flight") } in.Date = in.Date.In(time.UTC) out := &model.Flight{} err := s.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Try to find existing if err := tx.Where(`"number" = ? AND "from" = ? AND "to" = ? AND "date" = ?`, in.Number, in.From, in.To, in.Date).First(out).Error; err == nil { // found: do NOT update columns; only link relations below } else if errors.Is(err, gorm.ErrRecordNotFound) { // Not found → create (no user_id/card_id columns are written) cre := model.Flight{ Number: in.Number, From: in.From, To: in.To, Date: in.Date, } if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&cre).Error; err != nil { return err } // If conflict, fetch the existing one if cre.ID == 0 { if err := tx.Where(`"number" = ? AND "from" = ? AND "to" = ? AND "date" = ?`, in.Number, in.From, in.To, in.Date).First(&cre).Error; err != nil { return err } } *out = cre } else { return err } // ---- Link relations via associations (no raw IDs in the row) ---- // Link to User (many-to-many) if caller provided a UserID if in.UserID != 0 { u := model.User{ID: in.UserID} // Avoid dup join rows by adding a unique index on (user_id, flight_id). if err := tx.Model(out).Association("Users").Append(&u); err != nil { return err } } // Link to Card (many-to-many) if in.CardID != 0 { c := model.Card{ID: in.CardID} if err := tx.Model(out).Association("Cards").Append(&c); err != nil { return err } } return nil }) return out, err }