package sites import ( "context" "furtuna-be/internal/common/logger" "furtuna-be/internal/common/mycontext" "furtuna-be/internal/entity" "go.uber.org/zap" "gorm.io/gorm" ) type SiteRepository struct { db *gorm.DB } func NewSiteRepository(db *gorm.DB) *SiteRepository { return &SiteRepository{ db: db, } } func (r *SiteRepository) Upsert(ctx context.Context, site *entity.Site) (*entity.Site, error) { err := r.db.Transaction(func(tx *gorm.DB) error { if site.ID != 0 { // Update site if err := tx.Save(site).Error; err != nil { return err } } else { // Create new site if err := tx.Create(site).Error; err != nil { return err } } if len(site.Products) > 0 { for i := range site.Products { site.Products[i].SiteID = site.ID if site.Products[i].ID != 0 { // Update existing product if err := tx.Save(&site.Products[i]).Error; err != nil { return err } } else { // Create new product if err := tx.Create(&site.Products[i]).Error; err != nil { return err } } } } return nil }) if err != nil { logger.ContextLogger(ctx).Error("error when upserting site", zap.Error(err)) return nil, err } return site, nil } func (r *SiteRepository) Create(ctx context.Context, site *entity.SiteDB) (*entity.SiteDB, error) { err := r.db.Create(site).Error if err != nil { logger.ContextLogger(ctx).Error("error when creating site", zap.Error(err)) return nil, err } return site, nil } func (r *SiteRepository) Update(ctx context.Context, site *entity.SiteDB) (*entity.SiteDB, error) { if err := r.db.Save(site).Error; err != nil { logger.ContextLogger(ctx).Error("error when updating site", zap.Error(err)) return nil, err } return site, nil } func (r *SiteRepository) GetByID(ctx context.Context, id int64) (*entity.SiteDB, error) { site := new(entity.SiteDB) if err := r.db.First(site, id).Error; err != nil { logger.ContextLogger(ctx).Error("error when getting site by ID", zap.Error(err)) return nil, err } return site, nil } func (r *SiteRepository) GetAll(ctx context.Context, req entity.SiteSearch) (entity.SiteList, int, error) { var sites []*entity.SiteDB var total int64 query := r.db query = query.Where("deleted_at IS NULL") if req.Search != "" { query = query.Where("name ILIKE ?", "%"+req.Search+"%") } if req.Name != "" { query = query.Where("name ILIKE ?", "%"+req.Name+"%") } if req.PartnerID != nil { query = query.Where("partner_id = ?", req.PartnerID) } if req.SiteID != nil { query = query.Where("id = ?", req.SiteID) } if req.Limit > 0 { query = query.Limit(req.Limit) } if req.Offset > 0 { query = query.Offset(req.Offset) } if err := query.Find(&sites).Error; err != nil { logger.ContextLogger(ctx).Error("error when getting all sites", zap.Error(err)) return nil, 0, err } if err := r.db.Model(&entity.SiteDB{}).Where(query).Count(&total).Error; err != nil { logger.ContextLogger(ctx).Error("error when counting sites", zap.Error(err)) return nil, 0, err } return sites, int(total), nil } func (r *SiteRepository) Delete(ctx context.Context, id int64) error { site := new(entity.SiteDB) site.ID = id if err := r.db.Delete(site).Error; err != nil { return err } return nil } func (r *SiteRepository) Count(ctx mycontext.Context, req entity.SiteSearch) (*entity.SiteCountDB, error) { count := new(entity.SiteCountDB) query := r.db.Table("sites"). Select("count(*) as count") if !req.IsAdmin { query = query.Where("partner_id = ?", req.PartnerID) } if err := query.Scan(&count).Error; err != nil { logger.ContextLogger(ctx).Error("error when get count sites", zap.Error(err)) return nil, err } return count, nil } func (r *SiteRepository) GetNearestSites(ctx context.Context, latitude, longitude, radius float64) ([]entity.SiteProductInfo, error) { const limit = 5 var siteProducts []entity.SiteProductInfo distanceQuery := ` (6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))) ` // Primary query for sites within the radius err := r.db.WithContext(ctx).Raw(` SELECT s.id AS site_id, s.name AS site_name, s.region, s.regency, s.partner_id, s.image, s.address, s.location_link, s.description, s.highlight, s.contact_person, s.tnc, s.additional_info, s.status, s.is_season_ticket, s.is_discount_active, s.latitude, s.longitude, s.created_at, s.updated_at, `+distanceQuery+` AS distance, p.id AS product_id, p.name AS product_name, p.type AS product_type, p.price AS product_price, p.is_weekend_ticket, p.is_season_ticket, p.status AS product_status, p.description AS product_description FROM sites s LEFT JOIN ( SELECT *, ROW_NUMBER() OVER (PARTITION BY site_id ORDER BY price ASC) AS rn FROM products ) p ON s.id = p.site_id AND p.rn = 1 WHERE `+distanceQuery+` < ? ORDER BY distance LIMIT ?`, latitude, longitude, latitude, latitude, longitude, latitude, radius, limit).Scan(&siteProducts).Error if err != nil { return nil, err } // If fewer than 5 sites found, fetch additional ones regardless of distance if len(siteProducts) < limit { additionalLimit := limit - len(siteProducts) err = r.db.WithContext(ctx).Raw(` SELECT s.id AS site_id, s.name AS site_name, s.region, s.regency, s.partner_id, s.image, s.address, s.location_link, s.description, s.highlight, s.contact_person, s.tnc, s.additional_info, s.status, s.is_season_ticket, s.is_discount_active, s.latitude, s.longitude, s.created_at, s.updated_at, `+distanceQuery+` AS distance, p.id AS product_id, p.name AS product_name, p.type AS product_type, p.price AS product_price, p.is_weekend_ticket, p.is_season_ticket, p.status AS product_status, p.description AS product_description FROM sites s LEFT JOIN ( SELECT *, ROW_NUMBER() OVER (PARTITION BY site_id ORDER BY price ASC) AS rn FROM products ) p ON s.id = p.site_id AND p.rn = 1 ORDER BY distance LIMIT ?`, latitude, longitude, latitude, additionalLimit).Scan(&siteProducts).Error if err != nil { return nil, err } } return siteProducts, nil } func (r *SiteRepository) SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error) { var siteProducts []entity.SiteProductInfo var total int64 // Adding wildcard for partial matching searchName := "%" + search.Name + "%" // Base conditions and parameters conditions := "s.name ILIKE ?" params := []interface{}{searchName} // Add region filtering if region is provided if search.Region != "" { conditions += " AND s.region = ?" params = append(params, search.Region) } if search.Discover != "" { conditions += " AND s.address ILIKE ?" params = append(params, "%"+search.Discover+"%") } // Count query to get the total number of matching records countQuery := ` SELECT COUNT(*) FROM sites s WHERE ` + conditions err := r.db.WithContext(ctx).Raw(countQuery, params...).Scan(&total).Error if err != nil { return nil, 0, err } // Add limit and offset for the data query dataParams := append(params, search.Limit, search.Offset) // Primary query for sites matching name and region, with pagination dataQuery := ` SELECT s.id AS site_id, s.name AS site_name, s.region, s.regency, s.partner_id, s.image, s.address, s.location_link, s.description, s.highlight, s.contact_person, s.tnc, s.additional_info, s.status, s.is_season_ticket, s.is_discount_active, s.latitude, s.longitude, s.created_at, s.updated_at, p.id AS product_id, p.name AS product_name, p.type AS product_type, p.price AS product_price, p.is_weekend_ticket, p.is_season_ticket, p.status AS product_status, p.description AS product_description FROM sites s LEFT JOIN ( SELECT *, ROW_NUMBER() OVER (PARTITION BY site_id ORDER BY price ASC) AS rn FROM products ) p ON s.id = p.site_id AND p.rn = 1 WHERE ` + conditions + ` ORDER BY s.name LIMIT ? OFFSET ? ` err = r.db.WithContext(ctx).Raw(dataQuery, dataParams...).Scan(&siteProducts).Error if err != nil { return nil, 0, err } return siteProducts, total, nil }