saveinmed/backend/internal/domain/distance.go
Tiago Yamamoto 4bb848788f feat: tenant model, seeder, and product search with distance
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
2025-12-20 09:03:13 -03:00

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"`
}