Add Customer Discovery
This commit is contained in:
parent
aeed6fde7b
commit
600d42d529
@ -32,6 +32,7 @@ type Config struct {
|
|||||||
Brevo Brevo `mapstructure:"brevo"`
|
Brevo Brevo `mapstructure:"brevo"`
|
||||||
Email Email `mapstructure:"email"`
|
Email Email `mapstructure:"email"`
|
||||||
Withdraw Withdraw `mapstructure:"withdrawal"`
|
Withdraw Withdraw `mapstructure:"withdrawal"`
|
||||||
|
Discovery Discovery `mapstructure:"discovery"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
17
config/discovery.go
Normal file
17
config/discovery.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Discovery struct {
|
||||||
|
ExploreDestinations []ExploreDestination `mapstructure:"explore_destinations"`
|
||||||
|
ExploreRegions []ExploreRegion `mapstructure:"explore_regions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExploreDestinations []ExploreDestination
|
||||||
|
|
||||||
|
type ExploreDestination struct {
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
ImageURL string `mapstructure:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExploreRegion struct {
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
}
|
||||||
@ -25,7 +25,7 @@ postgresql:
|
|||||||
max-idle-connections-in-second: 600
|
max-idle-connections-in-second: 600
|
||||||
max-open-connections-in-second: 600
|
max-open-connections-in-second: 600
|
||||||
connection-max-life-time-in-second: 600
|
connection-max-life-time-in-second: 600
|
||||||
debug: true
|
debug: false
|
||||||
|
|
||||||
oss:
|
oss:
|
||||||
access_key_id: e50b31e5eddf63c0ZKB2
|
access_key_id: e50b31e5eddf63c0ZKB2
|
||||||
@ -54,3 +54,23 @@ email:
|
|||||||
|
|
||||||
withdrawal:
|
withdrawal:
|
||||||
platform_fee: 5000
|
platform_fee: 5000
|
||||||
|
|
||||||
|
discovery:
|
||||||
|
explore_destinations:
|
||||||
|
- name: "Jakarta"
|
||||||
|
image_url: "https://obs.eranyacloud.com/furtuna-dev/file/03c0b046-43ab-4d35-a743-6a173bc66b90-1722680749.png"
|
||||||
|
- name: "Banten"
|
||||||
|
image_url: "https://obs.eranyacloud.com/furtuna-dev/file/c8e7dd8a-17be-449f-afdc-0c07eda438ce-1722680809.png"
|
||||||
|
- name: "Yogyakarta"
|
||||||
|
image_url: "https://obs.eranyacloud.com/furtuna-dev/file/83b78c19-4c97-48c9-bc97-a7403e1c4eed-1722680828.png"
|
||||||
|
- name: "Jawa Barat"
|
||||||
|
image_url: "https://obs.eranyacloud.com/furtuna-dev/file/07c35ab1-3e20-4858-8d7d-b29517239dc3-1722680848.png"
|
||||||
|
- name: "Jawa Tengah"
|
||||||
|
image_url: "https://obs.eranyacloud.com/furtuna-dev/file/a1915a98-c2aa-4997-8e75-bd4e43789b0c-1722680874.png"
|
||||||
|
- name: "Jawa Timur"
|
||||||
|
image_url: "https://obs.eranyacloud.com/furtuna-dev/file/7b5d2b86-e8a8-4703-a153-c186021cf088-1722680894.png"
|
||||||
|
explore_regions:
|
||||||
|
- name: "Jawa"
|
||||||
|
- name: "Sumatera"
|
||||||
|
- name: "Kalimantan"
|
||||||
|
- name: "Sulawesi"
|
||||||
39
internal/entity/discovery.go
Normal file
39
internal/entity/discovery.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package entity
|
||||||
|
|
||||||
|
type DiscoverySearch struct {
|
||||||
|
Lat float64
|
||||||
|
Long float64
|
||||||
|
Name string
|
||||||
|
Region string
|
||||||
|
Discover string
|
||||||
|
Offset int
|
||||||
|
Limit int
|
||||||
|
Radius int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscoverySearchResp struct {
|
||||||
|
ExploreRegions []ExploreRegion `json:"exploreRegions"`
|
||||||
|
ExploreDestinations []ExploreDestination `json:"exploreDestinations"`
|
||||||
|
MustVisit []MustVisit `json:"mustVisit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExploreRegion struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExploreDestination struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MustVisit struct {
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
Rating float64 `json:"rating"`
|
||||||
|
ReviewCount int `json:"reviewCount"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Regency string `json:"regency"`
|
||||||
|
}
|
||||||
@ -25,6 +25,10 @@ type Site struct {
|
|||||||
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
||||||
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
|
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
|
||||||
Products []Product `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"`
|
Products []Product `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"`
|
||||||
|
Latitude *float64 `json:"latitude"`
|
||||||
|
Longitude *float64 `json:"longitude"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Distance float64 `json:"distance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SiteSearch struct {
|
type SiteSearch struct {
|
||||||
@ -166,3 +170,34 @@ func (e *SiteCountDB) ToSiteCount() *SiteCount {
|
|||||||
Count: e.Count,
|
Count: e.Count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SiteProductInfo struct {
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
SiteName string `json:"site_name"`
|
||||||
|
PartnerID int64 `json:"partner_id"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
LocationLink string `json:"location_link"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Highlight string `json:"highlight"`
|
||||||
|
ContactPerson string `json:"contact_person"`
|
||||||
|
TnC string `json:"tnc"`
|
||||||
|
AdditionalInfo string `json:"additional_info"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
IsSeasonTicket bool `json:"is_season_ticket"`
|
||||||
|
IsDiscountActive bool `json:"is_discount_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Distance float64 `json:"distance"` // Calculated field
|
||||||
|
ProductID int64 `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductType string `json:"product_type"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
IsWeekendTicket bool `json:"is_weekend_ticket"`
|
||||||
|
ProductStatus string `json:"product_status"`
|
||||||
|
ProductDescription string `json:"product_description"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Regency string `json:"regency"`
|
||||||
|
}
|
||||||
|
|||||||
134
internal/handlers/http/discovery/discover.go
Normal file
134
internal/handlers/http/discovery/discover.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"furtuna-be/internal/entity"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"furtuna-be/internal/common/errors"
|
||||||
|
"furtuna-be/internal/handlers/request"
|
||||||
|
"furtuna-be/internal/handlers/response"
|
||||||
|
"furtuna-be/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service services.DiscoverService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||||
|
route := group.Group("/discovery")
|
||||||
|
|
||||||
|
route.GET("/home", h.DisoveryHome)
|
||||||
|
route.GET("/search", h.DisoverySearch)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service services.DiscoverService) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) DisoveryHome(c *gin.Context) {
|
||||||
|
var req request.DiscoveryHomeParam
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.service.Home(c.Request.Context(), req.ToEntity())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: ConvertEntityToResponse(res),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) DisoverySearch(c *gin.Context) {
|
||||||
|
var req request.DiscoveryHomeParam
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, total, err := h.service.Search(c.Request.Context(), req.ToEntity())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: ConvertEntityToSearchResponse(res, total, req),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertEntityToResponse(entityResp *entity.DiscoverySearchResp) *response.ExploreResponse {
|
||||||
|
// Convert ExploreRegions
|
||||||
|
exploreRegions := make([]response.Region, len(entityResp.ExploreRegions))
|
||||||
|
for i, region := range entityResp.ExploreRegions {
|
||||||
|
exploreRegions[i] = response.Region{
|
||||||
|
Name: region.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert ExploreDestinations
|
||||||
|
exploreDestinations := make([]response.Destination, len(entityResp.ExploreDestinations))
|
||||||
|
for i, destination := range entityResp.ExploreDestinations {
|
||||||
|
exploreDestinations[i] = response.Destination{
|
||||||
|
Name: destination.Name,
|
||||||
|
ImageURL: destination.ImageURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mustVisit := make([]response.MustVisit, len(entityResp.MustVisit))
|
||||||
|
for i, mv := range entityResp.MustVisit {
|
||||||
|
mustVisit[i] = response.MustVisit{
|
||||||
|
Name: mv.Name,
|
||||||
|
Region: mv.Region,
|
||||||
|
Rating: mv.Rating,
|
||||||
|
ReviewCount: mv.ReviewCount,
|
||||||
|
Price: mv.Price,
|
||||||
|
ImageURL: mv.ImageURL,
|
||||||
|
SiteID: mv.SiteID,
|
||||||
|
Regency: mv.Regency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.ExploreResponse{
|
||||||
|
ExploreRegions: exploreRegions,
|
||||||
|
ExploreDestinations: exploreDestinations,
|
||||||
|
MustVisit: mustVisit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertEntityToSearchResponse(entityResp *entity.DiscoverySearchResp, total int64, req request.DiscoveryHomeParam) *response.SearchResponse {
|
||||||
|
data := make([]response.SiteSeach, len(entityResp.MustVisit))
|
||||||
|
for i, mv := range entityResp.MustVisit {
|
||||||
|
data[i] = response.SiteSeach{
|
||||||
|
Name: mv.Name,
|
||||||
|
Region: mv.Region,
|
||||||
|
Rating: mv.Rating,
|
||||||
|
ReviewCount: mv.ReviewCount,
|
||||||
|
Price: mv.Price,
|
||||||
|
ImageURL: mv.ImageURL,
|
||||||
|
SiteID: mv.SiteID,
|
||||||
|
Regency: mv.Regency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.SearchResponse{
|
||||||
|
Data: data,
|
||||||
|
Total: int(total),
|
||||||
|
Limit: req.Limit,
|
||||||
|
Offset: req.Offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
33
internal/handlers/request/discovery.go
Normal file
33
internal/handlers/request/discovery.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"furtuna-be/internal/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscoveryHomeParam struct {
|
||||||
|
Lat float64 `form:"lat" json:"lat" example:"10"`
|
||||||
|
Long float64 `form:"long" json:"long" example:"0"`
|
||||||
|
Name string `form:"name" json:"name" example:"0"`
|
||||||
|
Region string `form:"region" json:"region" example:"0"`
|
||||||
|
Radius int `form:"radius" json:"radius" example:"0"`
|
||||||
|
Limit int `form:"limit" json:"limit" example:"0"`
|
||||||
|
Offset int `form:"offset" json:"offset" example:"0"`
|
||||||
|
Discover string `form:"discover" json:"discover" example:"0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscoveryHomeParam) ToEntity() *entity.DiscoverySearch {
|
||||||
|
if d.Limit == 0 {
|
||||||
|
d.Limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entity.DiscoverySearch{
|
||||||
|
Lat: d.Lat,
|
||||||
|
Long: d.Long,
|
||||||
|
Name: d.Name,
|
||||||
|
Region: d.Region,
|
||||||
|
Radius: d.Radius,
|
||||||
|
Limit: d.Limit,
|
||||||
|
Offset: d.Offset,
|
||||||
|
Discover: d.Discover,
|
||||||
|
}
|
||||||
|
}
|
||||||
49
internal/handlers/response/discovery.go
Normal file
49
internal/handlers/response/discovery.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
type ExploreResponse struct {
|
||||||
|
ExploreRegions []Region `json:"exploreRegions"`
|
||||||
|
ExploreDestinations []Destination `json:"exploreDestinations"`
|
||||||
|
MustVisit []MustVisit `json:"mustVisit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CurrentLocation struct {
|
||||||
|
City string `json:"city"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Region struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Destination struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MustVisit struct {
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Rating float64 `json:"rating"`
|
||||||
|
ReviewCount int `json:"reviewCount"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
Regency string `json:"regency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResponse struct {
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Data []SiteSeach `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SiteSeach struct {
|
||||||
|
SiteID int64 `json:"site_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Rating float64 `json:"rating"`
|
||||||
|
ReviewCount int `json:"reviewCount"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
Regency string `json:"regency"`
|
||||||
|
}
|
||||||
@ -172,6 +172,8 @@ type SiteRepository interface {
|
|||||||
GetAll(ctx context.Context, req entity.SiteSearch) (entity.SiteList, int, error)
|
GetAll(ctx context.Context, req entity.SiteSearch) (entity.SiteList, int, error)
|
||||||
Delete(ctx context.Context, id int64) error
|
Delete(ctx context.Context, id int64) error
|
||||||
Count(ctx mycontext.Context, req entity.SiteSearch) (*entity.SiteCountDB, error)
|
Count(ctx mycontext.Context, req entity.SiteSearch) (*entity.SiteCountDB, error)
|
||||||
|
GetNearestSites(ctx context.Context, latitude, longitude, radius float64) ([]entity.SiteProductInfo, error)
|
||||||
|
SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionManager interface {
|
type TransactionManager interface {
|
||||||
|
|||||||
@ -156,3 +156,119 @@ func (r *SiteRepository) Count(ctx mycontext.Context, req entity.SiteSearch) (*e
|
|||||||
|
|
||||||
return count, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
25
internal/routes/customer_routes.go
Normal file
25
internal/routes/customer_routes.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"furtuna-be/internal/handlers/http/discovery"
|
||||||
|
"furtuna-be/internal/middlewares"
|
||||||
|
|
||||||
|
"furtuna-be/internal/app"
|
||||||
|
"furtuna-be/internal/repository"
|
||||||
|
"furtuna-be/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceManagerImpl,
|
||||||
|
repoManager *repository.RepoManagerImpl) {
|
||||||
|
approute := app.Group("/api/v1/customer")
|
||||||
|
|
||||||
|
authMiddleware := middlewares.AuthorizationMiddleware(repoManager.Crypto)
|
||||||
|
|
||||||
|
serverRoutes := []HTTPHandlerRoutes{
|
||||||
|
discovery.NewHandler(serviceManager.DiscoverService),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, handler := range serverRoutes {
|
||||||
|
handler.Route(approute, authMiddleware)
|
||||||
|
}
|
||||||
|
}
|
||||||
121
internal/services/discovery/discovery.go
Normal file
121
internal/services/discovery/discovery.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"furtuna-be/config"
|
||||||
|
|
||||||
|
"furtuna-be/internal/entity"
|
||||||
|
"furtuna-be/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultLatitude = -6.2088
|
||||||
|
defaultLongitude = 106.8456
|
||||||
|
radius = 10000
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscoveryService struct {
|
||||||
|
repo repository.SiteRepository
|
||||||
|
cfg config.Discovery
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDiscoveryService(repo repository.SiteRepository, cfg config.Discovery) *DiscoveryService {
|
||||||
|
return &DiscoveryService{
|
||||||
|
repo: repo,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DiscoveryService) Home(ctx context.Context, search *entity.DiscoverySearch) (*entity.DiscoverySearchResp, error) {
|
||||||
|
if search.Lat == 0 || search.Long == 0 {
|
||||||
|
search.Lat = defaultLatitude
|
||||||
|
search.Long = defaultLongitude
|
||||||
|
}
|
||||||
|
|
||||||
|
siteProducts, err := s.repo.GetNearestSites(ctx, search.Lat, search.Long, radius)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
exploreDestinations := []entity.ExploreDestination{}
|
||||||
|
for _, exploreDestination := range s.cfg.ExploreDestinations {
|
||||||
|
exploreDestinations = append(exploreDestinations, entity.ExploreDestination{
|
||||||
|
Name: exploreDestination.Name,
|
||||||
|
ImageURL: exploreDestination.ImageURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exploreRegions := []entity.ExploreRegion{}
|
||||||
|
for _, exploreRegion := range s.cfg.ExploreRegions {
|
||||||
|
exploreRegions = append(exploreRegions, entity.ExploreRegion{
|
||||||
|
Name: exploreRegion.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mustVisits := []entity.MustVisit{}
|
||||||
|
|
||||||
|
for _, siteProduct := range siteProducts {
|
||||||
|
mustVisits = append(mustVisits, entity.MustVisit{
|
||||||
|
Name: siteProduct.SiteName,
|
||||||
|
Price: siteProduct.ProductPrice,
|
||||||
|
Region: siteProduct.Region,
|
||||||
|
SiteID: siteProduct.SiteID,
|
||||||
|
ImageURL: siteProduct.Image,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &entity.DiscoverySearchResp{
|
||||||
|
ExploreRegions: exploreRegions,
|
||||||
|
ExploreDestinations: exploreDestinations,
|
||||||
|
MustVisit: mustVisits,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DiscoveryService) Search(ctx context.Context, search *entity.DiscoverySearch) (*entity.DiscoverySearchResp, int64, error) {
|
||||||
|
if search.Lat == 0 || search.Long == 0 {
|
||||||
|
search.Lat = defaultLatitude
|
||||||
|
search.Long = defaultLongitude
|
||||||
|
search.Radius = radius
|
||||||
|
}
|
||||||
|
|
||||||
|
siteProducts, total, err := s.repo.SearchSites(ctx, search)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
exploreDestinations := []entity.ExploreDestination{}
|
||||||
|
for _, exploreDestination := range s.cfg.ExploreDestinations {
|
||||||
|
exploreDestinations = append(exploreDestinations, entity.ExploreDestination{
|
||||||
|
Name: exploreDestination.Name,
|
||||||
|
ImageURL: exploreDestination.ImageURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exploreRegions := []entity.ExploreRegion{}
|
||||||
|
for _, exploreRegion := range s.cfg.ExploreRegions {
|
||||||
|
exploreRegions = append(exploreRegions, entity.ExploreRegion{
|
||||||
|
Name: exploreRegion.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mustVisits := []entity.MustVisit{}
|
||||||
|
|
||||||
|
for _, siteProduct := range siteProducts {
|
||||||
|
mustVisits = append(mustVisits, entity.MustVisit{
|
||||||
|
Name: siteProduct.SiteName,
|
||||||
|
Price: siteProduct.ProductPrice,
|
||||||
|
Region: siteProduct.Region,
|
||||||
|
SiteID: siteProduct.SiteID,
|
||||||
|
ImageURL: siteProduct.Image,
|
||||||
|
Regency: siteProduct.Regency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &entity.DiscoverySearchResp{
|
||||||
|
ExploreRegions: exploreRegions,
|
||||||
|
ExploreDestinations: exploreDestinations,
|
||||||
|
MustVisit: mustVisits,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, total, nil
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"furtuna-be/internal/common/mycontext"
|
"furtuna-be/internal/common/mycontext"
|
||||||
"furtuna-be/internal/services/balance"
|
"furtuna-be/internal/services/balance"
|
||||||
"furtuna-be/internal/services/branch"
|
"furtuna-be/internal/services/branch"
|
||||||
|
"furtuna-be/internal/services/discovery"
|
||||||
service "furtuna-be/internal/services/license"
|
service "furtuna-be/internal/services/license"
|
||||||
"furtuna-be/internal/services/order"
|
"furtuna-be/internal/services/order"
|
||||||
"furtuna-be/internal/services/oss"
|
"furtuna-be/internal/services/oss"
|
||||||
@ -25,19 +26,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ServiceManagerImpl struct {
|
type ServiceManagerImpl struct {
|
||||||
AuthSvc Auth
|
AuthSvc Auth
|
||||||
EventSvc Event
|
EventSvc Event
|
||||||
UserSvc User
|
UserSvc User
|
||||||
BranchSvc Branch
|
BranchSvc Branch
|
||||||
StudioSvc Studio
|
StudioSvc Studio
|
||||||
ProductSvc Product
|
ProductSvc Product
|
||||||
OrderSvc Order
|
OrderSvc Order
|
||||||
OSSSvc OSSService
|
OSSSvc OSSService
|
||||||
PartnerSvc Partner
|
PartnerSvc Partner
|
||||||
SiteSvc Site
|
SiteSvc Site
|
||||||
LicenseSvc License
|
LicenseSvc License
|
||||||
Transaction Transaction
|
Transaction Transaction
|
||||||
Balance Balance
|
Balance Balance
|
||||||
|
DiscoverService DiscoverService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
|
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
|
||||||
@ -52,10 +54,11 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
|||||||
OSSSvc: oss.NewOSSService(repo.OSS),
|
OSSSvc: oss.NewOSSService(repo.OSS),
|
||||||
PartnerSvc: partner.NewPartnerService(
|
PartnerSvc: partner.NewPartnerService(
|
||||||
repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet),
|
repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet),
|
||||||
SiteSvc: site.NewSiteService(repo.Site),
|
SiteSvc: site.NewSiteService(repo.Site),
|
||||||
LicenseSvc: service.NewLicenseService(repo.License),
|
LicenseSvc: service.NewLicenseService(repo.License),
|
||||||
Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
|
Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
|
||||||
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
|
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
|
||||||
|
DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,3 +159,8 @@ type Balance interface {
|
|||||||
WithdrawInquiry(ctx context.Context, req *entity.BalanceWithdrawInquiry) (*entity.BalanceWithdrawInquiryResponse, error)
|
WithdrawInquiry(ctx context.Context, req *entity.BalanceWithdrawInquiry) (*entity.BalanceWithdrawInquiryResponse, error)
|
||||||
WithdrawExecute(ctx mycontext.Context, req *entity.WalletWithdrawRequest) (*entity.WalletWithdrawResponse, error)
|
WithdrawExecute(ctx mycontext.Context, req *entity.WalletWithdrawRequest) (*entity.WalletWithdrawResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiscoverService interface {
|
||||||
|
Home(ctx context.Context, search *entity.DiscoverySearch) (*entity.DiscoverySearchResp, error)
|
||||||
|
Search(ctx context.Context, search *entity.DiscoverySearch) (*entity.DiscoverySearchResp, int64, error)
|
||||||
|
}
|
||||||
|
|||||||
1
main.go
1
main.go
@ -32,6 +32,7 @@ func main() {
|
|||||||
|
|
||||||
routes.RegisterPublicRoutes(server, service, repo)
|
routes.RegisterPublicRoutes(server, service, repo)
|
||||||
routes.RegisterPrivateRoutes(server, service, repo)
|
routes.RegisterPrivateRoutes(server, service, repo)
|
||||||
|
routes.RegisterCustomerRoutes(server, service, repo)
|
||||||
|
|
||||||
server.StartScheduler()
|
server.StartScheduler()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user