package repository import ( "context" "errors" "git.techease.ru/Smart-search/smart-search-back/internal/model" "git.techease.ru/Smart-search/smart-search-back/pkg/crypto" errs "git.techease.ru/Smart-search/smart-search-back/pkg/errors" sq "github.com/Masterminds/squirrel" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type userRepository struct { pool *pgxpool.Pool qb sq.StatementBuilderType cryptoHelper *crypto.Crypto } func NewUserRepository(pool *pgxpool.Pool, cryptoSecret string) UserRepository { return &userRepository{ pool: pool, qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), cryptoHelper: crypto.NewCrypto(cryptoSecret), } } func (r *userRepository) FindByEmailHash(ctx context.Context, emailHash string) (*model.User, error) { query := r.qb.Select( "id", "email", "email_hash", "password_hash", "phone", "user_name", "company_name", "balance", "payment_status", "invites_issued", "invites_limit", "created_at", ).From("users").Where(sq.Eq{"email_hash": emailHash}) sqlQuery, args, err := query.ToSql() if err != nil { return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err) } user := &model.User{} err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan( &user.ID, &user.Email, &user.EmailHash, &user.PasswordHash, &user.Phone, &user.UserName, &user.CompanyName, &user.Balance, &user.PaymentStatus, &user.InvitesIssued, &user.InvitesLimit, &user.CreatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return nil, errs.NewBusinessError(errs.UserNotFound, "user not found") } if err != nil { return nil, errs.NewInternalError(errs.DatabaseError, "failed to find user", err) } return user, nil } func (r *userRepository) FindByID(ctx context.Context, userID int) (*model.User, error) { query := r.qb.Select( "id", "email", "email_hash", "password_hash", "phone", "user_name", "company_name", "balance", "payment_status", "invites_issued", "invites_limit", "created_at", ).From("users").Where(sq.Eq{"id": userID}) sqlQuery, args, err := query.ToSql() if err != nil { return nil, errs.NewInternalError(errs.DatabaseError, "failed to build query", err) } user := &model.User{} err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan( &user.ID, &user.Email, &user.EmailHash, &user.PasswordHash, &user.Phone, &user.UserName, &user.CompanyName, &user.Balance, &user.PaymentStatus, &user.InvitesIssued, &user.InvitesLimit, &user.CreatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return nil, errs.NewBusinessError(errs.UserNotFound, "user not found") } if err != nil { return nil, errs.NewInternalError(errs.DatabaseError, "failed to find user", err) } return user, nil } func (r *userRepository) Create(ctx context.Context, user *model.User) error { return r.createWithExecutor(ctx, r.pool, user) } func (r *userRepository) CreateTx(ctx context.Context, tx pgx.Tx, user *model.User) error { return r.createWithExecutor(ctx, tx, user) } func (r *userRepository) createWithExecutor(ctx context.Context, exec DBTX, user *model.User) error { encryptedEmail, err := r.cryptoHelper.Encrypt(user.Email) if err != nil { return errs.NewInternalError(errs.EncryptionError, "failed to encrypt email", err) } encryptedPhone, err := r.cryptoHelper.Encrypt(user.Phone) if err != nil { return errs.NewInternalError(errs.EncryptionError, "failed to encrypt phone", err) } encryptedUserName, err := r.cryptoHelper.Encrypt(user.UserName) if err != nil { return errs.NewInternalError(errs.EncryptionError, "failed to encrypt user name", err) } query := r.qb.Insert("users").Columns( "email", "email_hash", "password_hash", "phone", "user_name", "company_name", "balance", "payment_status", ).Values( encryptedEmail, user.EmailHash, user.PasswordHash, encryptedPhone, encryptedUserName, user.CompanyName, user.Balance, user.PaymentStatus, ).Suffix("RETURNING id") sqlQuery, args, err := query.ToSql() if err != nil { return errs.NewInternalError(errs.DatabaseError, "failed to build query", err) } err = exec.QueryRow(ctx, sqlQuery, args...).Scan(&user.ID) if err != nil { return errs.NewInternalError(errs.DatabaseError, "failed to create user", err) } return nil } func (r *userRepository) UpdateBalance(ctx context.Context, userID int, delta float64) error { return r.updateBalanceWithExecutor(ctx, r.pool, userID, delta) } func (r *userRepository) UpdateBalanceTx(ctx context.Context, tx pgx.Tx, userID int, delta float64) error { return r.updateBalanceWithExecutor(ctx, tx, userID, delta) } func (r *userRepository) updateBalanceWithExecutor(ctx context.Context, exec DBTX, userID int, delta float64) error { query := r.qb.Update("users"). Set("balance", sq.Expr("balance + ?", delta)). Where(sq.And{ sq.Eq{"id": userID}, sq.Expr("balance + ? >= 0", delta), }) sqlQuery, args, err := query.ToSql() if err != nil { return errs.NewInternalError(errs.DatabaseError, "failed to build query", err) } result, err := exec.Exec(ctx, sqlQuery, args...) if err != nil { return errs.NewInternalError(errs.DatabaseError, "failed to update balance", err) } if result.RowsAffected() == 0 { return errs.NewBusinessError(errs.InsufficientBalance, "insufficient balance") } return nil } func (r *userRepository) GetBalance(ctx context.Context, userID int) (float64, error) { query := r.qb.Select("balance").From("users").Where(sq.Eq{"id": userID}) sqlQuery, args, err := query.ToSql() if err != nil { return 0, errs.NewInternalError(errs.DatabaseError, "failed to build query", err) } var balance float64 err = r.pool.QueryRow(ctx, sqlQuery, args...).Scan(&balance) if errors.Is(err, pgx.ErrNoRows) { return 0, errs.NewBusinessError(errs.UserNotFound, "user not found") } if err != nil { return 0, errs.NewInternalError(errs.DatabaseError, "failed to get balance", err) } return balance, nil } func (r *userRepository) IncrementInvitesIssued(ctx context.Context, userID int) error { return r.incrementInvitesIssuedWithExecutor(ctx, r.pool, userID) } func (r *userRepository) IncrementInvitesIssuedTx(ctx context.Context, tx pgx.Tx, userID int) error { return r.incrementInvitesIssuedWithExecutor(ctx, tx, userID) } func (r *userRepository) incrementInvitesIssuedWithExecutor(ctx context.Context, exec DBTX, userID int) error { query := r.qb.Update("users"). Set("invites_issued", sq.Expr("invites_issued + 1")). Where(sq.And{ sq.Eq{"id": userID}, sq.Expr("invites_issued < invites_limit"), }) sqlQuery, args, err := query.ToSql() if err != nil { return errs.NewInternalError(errs.DatabaseError, "failed to build query", err) } result, err := exec.Exec(ctx, sqlQuery, args...) if err != nil { return errs.NewInternalError(errs.DatabaseError, "failed to increment invites issued", err) } if result.RowsAffected() == 0 { return errs.NewBusinessError(errs.InviteLimitReached, "invite limit reached") } return nil } func (r *userRepository) CheckInviteLimit(ctx context.Context, userID int) (bool, error) { return r.checkInviteLimitWithExecutor(ctx, r.pool, userID) } func (r *userRepository) CheckInviteLimitTx(ctx context.Context, tx pgx.Tx, userID int) (bool, error) { return r.checkInviteLimitWithExecutor(ctx, tx, userID) } func (r *userRepository) checkInviteLimitWithExecutor(ctx context.Context, exec DBTX, userID int) (bool, error) { query := r.qb.Select("invites_issued", "invites_limit"). From("users"). Where(sq.Eq{"id": userID}). Suffix("FOR UPDATE") sqlQuery, args, err := query.ToSql() if err != nil { return false, errs.NewInternalError(errs.DatabaseError, "failed to build query", err) } var issued, limit int err = exec.QueryRow(ctx, sqlQuery, args...).Scan(&issued, &limit) if err != nil { return false, errs.NewInternalError(errs.DatabaseError, "failed to check invite limit", err) } return issued < limit, nil }