Tenant Model: - Renamed Company→Tenant (Company alias for compatibility) - Added: lat/lng, city, state, category - Updated: postgres, handlers, DTOs, schema SQL Seeder (cmd/seeder): - Generates 400 pharmacies in Anápolis/GO - 20-500 products per tenant - Haversine distance variation ±5km from center Product Search: - GET /products/search with advanced filters - Filters: price (min/max), expiration, distance - Haversine distance calculation (approx km) - Anonymous seller (only city/state shown until checkout) - Ordered by expiration date (nearest first) New domain types: - ProductWithDistance, ProductSearchFilter, ProductSearchPage - HaversineDistance function Updated tests for Category instead of Role
65 lines
1.8 KiB
Go
65 lines
1.8 KiB
Go
package domain
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
const earthRadiusKm = 6371.0
|
|
|
|
// HaversineDistance calculates the distance in kilometers between two points
|
|
// using the Haversine formula. The result is approximate (±5km for security).
|
|
func HaversineDistance(lat1, lng1, lat2, lng2 float64) float64 {
|
|
dLat := degreesToRadians(lat2 - lat1)
|
|
dLng := degreesToRadians(lng2 - lng1)
|
|
|
|
lat1Rad := degreesToRadians(lat1)
|
|
lat2Rad := degreesToRadians(lat2)
|
|
|
|
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
|
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
|
|
math.Sin(dLng/2)*math.Sin(dLng/2)
|
|
|
|
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
|
|
|
distance := earthRadiusKm * c
|
|
|
|
// Round to nearest km for approximate distance (security)
|
|
return math.Round(distance)
|
|
}
|
|
|
|
func degreesToRadians(degrees float64) float64 {
|
|
return degrees * math.Pi / 180
|
|
}
|
|
|
|
// ProductWithDistance extends Product with distance information for search results.
|
|
type ProductWithDistance struct {
|
|
Product
|
|
DistanceKm float64 `json:"distance_km"`
|
|
TenantCity string `json:"tenant_city,omitempty"`
|
|
TenantState string `json:"tenant_state,omitempty"`
|
|
// TenantID is hidden for anonymous browsing, revealed only at checkout
|
|
}
|
|
|
|
// ProductSearchFilter captures search constraints.
|
|
type ProductSearchFilter struct {
|
|
Search string
|
|
Category string
|
|
MinPriceCents *int64
|
|
MaxPriceCents *int64
|
|
ExpiresAfter *time.Time
|
|
ExpiresBefore *time.Time
|
|
MaxDistanceKm *float64
|
|
BuyerLat float64
|
|
BuyerLng float64
|
|
Limit int
|
|
Offset int
|
|
}
|
|
|
|
// ProductSearchPage wraps search results with pagination.
|
|
type ProductSearchPage struct {
|
|
Products []ProductWithDistance `json:"products"`
|
|
Total int64 `json:"total"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
}
|