359 lines
10 KiB
Go
359 lines
10 KiB
Go
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
|
|
}
|