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, PrepareStmt: true, 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{}) } 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 { // 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 } } // 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 } // create if err := tx.Create(in).Error; err != nil { return err } *out = *in return nil }) return out, err } 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 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 if err := tx.Create(in).Error; err != nil { return err } *out = *in return nil }) return out, err } 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 } else if errors.Is(err, gorm.ErrRecordNotFound) { // not found → create 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 } // on 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 } // many-to-many user-flight if in.UserID != 0 { u := model.User{ID: in.UserID} if err := tx.Model(out).Association("Users").Append(&u); err != nil { return err } } // many-to-many flight-card 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 }