package repository import ( "enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/entity" "enaklo-pos-be/internal/repository/models" "fmt" "github.com/google/uuid" "github.com/pkg/errors" "gorm.io/gorm" "math/rand" "time" ) type CustomerRepo interface { Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) AddPoints(ctx mycontext.Context, id int64, points int, reference string) error FindSequence(ctx mycontext.Context, partnerID int64) (int64, error) GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) } type customerRepository struct { db *gorm.DB } func NewCustomerRepository(db *gorm.DB) *customerRepository { return &customerRepository{db: db} } func (r *customerRepository) Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) { tx := r.db.Begin() if tx.Error != nil { return nil, errors.Wrap(tx.Error, "failed to begin transaction") } customerDB := r.toCustomerDBModel(customer) if err := tx.Omit("CustomerID").Create(&customerDB).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to insert customer") } customerPoints := models.CustomerPointsDB{ CustomerID: uint64(customerDB.ID), TotalPoints: 0, AvailablePoints: 0, LastUpdated: time.Now(), } if err := tx.Create(&customerPoints).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to create initial customer points") } otpCode := r.generateOTPCode() expiresAt := time.Now().Add(15 * time.Minute) verificationCode := models.CustomerVerificationCodeDB{ CustomerID: uint64(customerDB.ID), Code: otpCode, Type: "EMAIL", ExpiresAt: expiresAt, IsUsed: false, VerificationID: uuid.New(), } if err := tx.Create(&verificationCode).Error; err != nil { tx.Rollback() return nil, errors.Wrap(err, "failed to create verification code") } if err := tx.Commit().Error; err != nil { return nil, errors.Wrap(err, "failed to commit transaction") } customer.ID = customerDB.ID customer.VerificationID = verificationCode.VerificationID.String() customer.OTP = otpCode return customer, nil } func (r *customerRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) { var customerDB models.CustomerDB if err := r.db.First(&customerDB, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("customer not found") } return nil, errors.Wrap(err, "failed to find customer") } customer := r.toDomainCustomerModel(&customerDB) return customer, nil } func (r *customerRepository) FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) { var customerDB models.CustomerDB if err := r.db.Where("phone = ?", phone).First(&customerDB).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("customer not found") } return nil, errors.Wrap(err, "failed to find customer by phone") } customer := r.toDomainCustomerModel(&customerDB) return customer, nil } func (r *customerRepository) FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) { var customerDB models.CustomerDB if err := r.db.Where("email = ?", email).First(&customerDB).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("customer not found") } return nil, errors.Wrap(err, "failed to find customer by email") } customer := r.toDomainCustomerModel(&customerDB) return customer, nil } func (r *customerRepository) AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error { tx := r.db.Begin() if tx.Error != nil { return errors.Wrap(tx.Error, "failed to begin transaction") } result := tx.Model(&models.CustomerPointsDB{}). Where("customer_id = ?", customerID). Updates(map[string]interface{}{ "total_points": gorm.Expr("total_points + ?", points), "available_points": gorm.Expr("available_points + ?", points), "last_updated": time.Now(), }) if result.Error != nil { tx.Rollback() return errors.Wrap(result.Error, "failed to update customer points") } if result.RowsAffected == 0 { tx.Rollback() return errors.New("customer points record not found") } pointTransaction := models.CustomerPointTransactionDB{ CustomerID: customerID, Reference: reference, PointsEarned: points, TransactionDate: time.Now(), Status: "active", } if err := tx.Create(&pointTransaction).Error; err != nil { tx.Rollback() return errors.Wrap(err, "failed to create point transaction record") } if err := tx.Commit().Error; err != nil { return errors.Wrap(err, "failed to commit transaction") } return nil } func (r *customerRepository) toCustomerDBModel(customer *entity.Customer) models.CustomerDB { return models.CustomerDB{ ID: customer.ID, Name: customer.Name, Email: customer.Email, Phone: customer.Phone, Points: customer.Points, CreatedAt: customer.CreatedAt, UpdatedAt: customer.UpdatedAt, BirthDate: customer.BirthDate, Password: customer.Password, } } func (r *customerRepository) FindSequence(ctx mycontext.Context, partnerID int64) (int64, error) { tx := r.db.Begin() if tx.Error != nil { return 0, errors.Wrap(tx.Error, "failed to begin transaction") } defer func() { if r := recover(); r != nil { tx.Rollback() } }() var sequence models.PartnerMemberSequence result := tx.Where("partner_id = ?", partnerID).First(&sequence) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { now := time.Now() newSequence := models.PartnerMemberSequence{ PartnerID: partnerID, LastSequence: 1, UpdatedAt: now, } if err := tx.Create(&newSequence).Error; err != nil { tx.Rollback() return 0, errors.Wrap(err, "failed to create new sequence") } if err := tx.Commit().Error; err != nil { return 0, errors.Wrap(err, "failed to commit transaction") } return 1, nil } tx.Rollback() return 0, errors.Wrap(result.Error, "failed to query sequence") } newSequenceValue := sequence.LastSequence + 1 updates := map[string]interface{}{ "last_sequence": newSequenceValue, "updated_at": time.Now(), } if err := tx.Model(&sequence).Updates(updates).Error; err != nil { tx.Rollback() return 0, errors.Wrap(err, "failed to update sequence") } if err := tx.Commit().Error; err != nil { return 0, errors.Wrap(err, "failed to commit transaction") } return newSequenceValue, nil } func (r *customerRepository) toDomainCustomerModel(dbModel *models.CustomerDB) *entity.Customer { return &entity.Customer{ ID: dbModel.ID, Name: dbModel.Name, Email: dbModel.Email, Phone: dbModel.Phone, Points: dbModel.Points, CreatedAt: dbModel.CreatedAt, UpdatedAt: dbModel.UpdatedAt, CustomerID: dbModel.CustomerID, BirthDate: dbModel.BirthDate, Password: dbModel.Password, } } func (r *customerRepository) GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) { if req.Limit <= 0 { req.Limit = 10 } if req.Offset < 0 { req.Offset = 0 } query := r.db.Model(&models.CustomerDB{}) if req.Search != "" { searchTerm := "%" + req.Search + "%" query = query.Where( "name ILIKE ? OR email ILIKE ? OR phone ILIKE ?", searchTerm, searchTerm, searchTerm, ) } var totalCount int64 if err := query.Count(&totalCount).Error; err != nil { return nil, 0, errors.Wrap(err, "failed to count customers") } var customersDB []models.CustomerDB result := query. Order("created_at DESC"). Limit(req.Limit). Offset(req.Offset). Find(&customersDB) if result.Error != nil { return nil, 0, errors.Wrap(result.Error, "failed to retrieve customers") } customers := make(entity.MemberList, len(customersDB)) for i, customerDB := range customersDB { customers[i] = r.toDomainCustomerModel(&customerDB) } return customers, int(totalCount), nil } func (r *customerRepository) generateOTPCode() string { rand.Seed(time.Now().UnixNano()) otpCode := fmt.Sprintf("%06d", rand.Intn(1000000)) return otpCode } func (r *customerRepository) VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) { var verificationCode models.CustomerVerificationCodeDB if err := r.db.Where("verification_id = ? AND is_used = false", verificationHash).First(&verificationCode).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, errors.New("invalid or expired verification code") } return 0, errors.Wrap(err, "failed to find verification code") } if time.Now().After(verificationCode.ExpiresAt) { return 0, errors.New("verification code has expired") } if verificationCode.Code != otpCode { return 0, errors.New("invalid verification code") } tx := r.db.Begin() if tx.Error != nil { return 0, errors.Wrap(tx.Error, "failed to begin transaction") } if err := tx.Model(&verificationCode).Updates(map[string]interface{}{ "is_used": true, }).Error; err != nil { tx.Rollback() return 0, errors.Wrap(err, "failed to mark verification code as used") } if verificationCode.Type == "EMAIL" { if err := tx.Model(&models.CustomerDB{}).Where("id = ?", verificationCode.CustomerID). Update("is_email_verified", true).Error; err != nil { tx.Rollback() return 0, errors.Wrap(err, "failed to update customer verification status") } } else if verificationCode.Type == "PHONE" { if err := tx.Model(&models.CustomerDB{}).Where("id = ?", verificationCode.CustomerID). Update("is_phone_verified", true).Error; err != nil { tx.Rollback() return 0, errors.Wrap(err, "failed to update customer verification status") } } if err := tx.Commit().Error; err != nil { return 0, errors.Wrap(err, "failed to commit transaction") } return int64(verificationCode.CustomerID), nil }