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
200 lines
5.9 KiB
Go
200 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid/v5"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
// Anápolis, GO coordinates
|
|
const (
|
|
AnapolisLat = -16.3281
|
|
AnapolisLng = -48.9530
|
|
VarianceKm = 5.0 // ~5km spread
|
|
)
|
|
|
|
// Product categories with sample medicines
|
|
var categories = []string{
|
|
"Analgésicos",
|
|
"Antibióticos",
|
|
"Anti-inflamatórios",
|
|
"Cardiovasculares",
|
|
"Dermatológicos",
|
|
"Vitaminas",
|
|
"Oftálmicos",
|
|
"Respiratórios",
|
|
}
|
|
|
|
var medicamentos = []struct {
|
|
Name string
|
|
Category string
|
|
BasePrice int64
|
|
}{
|
|
{"Dipirona 500mg", "Analgésicos", 1299},
|
|
{"Paracetamol 750mg", "Analgésicos", 899},
|
|
{"Ibuprofeno 400mg", "Anti-inflamatórios", 1599},
|
|
{"Nimesulida 100mg", "Anti-inflamatórios", 2499},
|
|
{"Amoxicilina 500mg", "Antibióticos", 3499},
|
|
{"Azitromicina 500mg", "Antibióticos", 4999},
|
|
{"Losartana 50mg", "Cardiovasculares", 2199},
|
|
{"Atenolol 50mg", "Cardiovasculares", 1899},
|
|
{"Vitamina C 1g", "Vitaminas", 1599},
|
|
{"Vitamina D 2000UI", "Vitaminas", 2299},
|
|
{"Cetoconazol Creme", "Dermatológicos", 2899},
|
|
{"Dexametasona Creme", "Dermatológicos", 1999},
|
|
{"Colírio Lubrificante", "Oftálmicos", 3299},
|
|
{"Lacrifilm 15ml", "Oftálmicos", 2899},
|
|
{"Loratadina 10mg", "Respiratórios", 1799},
|
|
{"Dexclorfeniramina 2mg", "Respiratórios", 1599},
|
|
{"Omeprazol 20mg", "Gastrointestinais", 1999},
|
|
{"Pantoprazol 40mg", "Gastrointestinais", 2999},
|
|
{"Metformina 850mg", "Antidiabéticos", 1299},
|
|
{"Glibenclamida 5mg", "Antidiabéticos", 999},
|
|
}
|
|
|
|
var pharmacyNames = []string{
|
|
"Farmácia Popular", "Drogaria Central", "Farma Vida", "Drogasil",
|
|
"Farmácia São João", "Droga Raia", "Ultrafarma", "Farma Bem",
|
|
"Drogaria Moderna", "Farmácia União", "Saúde Total", "Bem Estar",
|
|
"Vida Saudável", "Mais Saúde", "Farmácia do Povo", "Super Farma",
|
|
}
|
|
|
|
func main() {
|
|
godotenv.Load()
|
|
dsn := os.Getenv("DATABASE_URL")
|
|
if dsn == "" {
|
|
log.Fatal("DATABASE_URL not set")
|
|
}
|
|
|
|
db, err := sqlx.Connect("pgx", dsn)
|
|
if err != nil {
|
|
log.Fatalf("db connect: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
ctx := context.Background()
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
// Generate 400 tenants
|
|
tenants := generateTenants(rng, 400)
|
|
log.Printf("Generated %d tenants", len(tenants))
|
|
|
|
// Insert tenants
|
|
for _, t := range tenants {
|
|
_, err := db.NamedExecContext(ctx, `
|
|
INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, created_at, updated_at)
|
|
VALUES (:id, :cnpj, :corporate_name, :category, :license_number, :is_verified, :latitude, :longitude, :city, :state, :created_at, :updated_at)
|
|
ON CONFLICT (cnpj) DO NOTHING`, t)
|
|
if err != nil {
|
|
log.Printf("insert tenant %s: %v", t["corporate_name"], err)
|
|
}
|
|
}
|
|
log.Println("Tenants inserted")
|
|
|
|
// Generate and insert products for each tenant
|
|
totalProducts := 0
|
|
for _, t := range tenants {
|
|
tenantID := t["id"].(uuid.UUID)
|
|
numProducts := 20 + rng.Intn(481) // 20-500
|
|
products := generateProducts(rng, tenantID, numProducts)
|
|
totalProducts += len(products)
|
|
|
|
for _, p := range products {
|
|
_, err := db.NamedExecContext(ctx, `
|
|
INSERT INTO products (id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at)
|
|
VALUES (:id, :seller_id, :name, :description, :batch, :expires_at, :price_cents, :stock, :created_at, :updated_at)
|
|
ON CONFLICT DO NOTHING`, p)
|
|
if err != nil {
|
|
log.Printf("insert product: %v", err)
|
|
}
|
|
}
|
|
}
|
|
log.Printf("Inserted %d products total", totalProducts)
|
|
|
|
// Output summary
|
|
summary := map[string]interface{}{
|
|
"tenants": len(tenants),
|
|
"products": totalProducts,
|
|
"location": "Anápolis, GO",
|
|
}
|
|
out, _ := json.MarshalIndent(summary, "", " ")
|
|
fmt.Println(string(out))
|
|
}
|
|
|
|
func generateTenants(rng *rand.Rand, count int) []map[string]interface{} {
|
|
now := time.Now().UTC()
|
|
tenants := make([]map[string]interface{}, 0, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
id, _ := uuid.NewV4()
|
|
name := fmt.Sprintf("%s %d", pharmacyNames[rng.Intn(len(pharmacyNames))], i+1)
|
|
cnpj := generateCNPJ(rng)
|
|
|
|
// Vary lat/lng within ~5km of Anápolis center
|
|
latOffset := (rng.Float64() - 0.5) * 0.09 // ~5km
|
|
lngOffset := (rng.Float64() - 0.5) * 0.09
|
|
|
|
tenants = append(tenants, map[string]interface{}{
|
|
"id": id,
|
|
"cnpj": cnpj,
|
|
"corporate_name": name,
|
|
"category": "farmacia",
|
|
"license_number": fmt.Sprintf("CRF-GO-%05d", rng.Intn(99999)),
|
|
"is_verified": rng.Float32() > 0.3, // 70% verified
|
|
"latitude": AnapolisLat + latOffset,
|
|
"longitude": AnapolisLng + lngOffset,
|
|
"city": "Anápolis",
|
|
"state": "GO",
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
})
|
|
}
|
|
return tenants
|
|
}
|
|
|
|
func generateProducts(rng *rand.Rand, sellerID uuid.UUID, count int) []map[string]interface{} {
|
|
now := time.Now().UTC()
|
|
products := make([]map[string]interface{}, 0, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
id, _ := uuid.NewV4()
|
|
med := medicamentos[rng.Intn(len(medicamentos))]
|
|
|
|
// Random expiration: 30 days to 2 years from now
|
|
daysToExpire := 30 + rng.Intn(700)
|
|
expiresAt := now.AddDate(0, 0, daysToExpire)
|
|
|
|
// Price variation: -20% to +30%
|
|
priceMultiplier := 0.8 + rng.Float64()*0.5
|
|
price := int64(float64(med.BasePrice) * priceMultiplier)
|
|
|
|
products = append(products, map[string]interface{}{
|
|
"id": id,
|
|
"seller_id": sellerID,
|
|
"name": med.Name,
|
|
"description": fmt.Sprintf("%s - %s", med.Name, med.Category),
|
|
"batch": fmt.Sprintf("L%d%03d", now.Year(), rng.Intn(999)),
|
|
"expires_at": expiresAt,
|
|
"price_cents": price,
|
|
"stock": int64(10 + rng.Intn(500)),
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
})
|
|
}
|
|
return products
|
|
}
|
|
|
|
func generateCNPJ(rng *rand.Rand) string {
|
|
return fmt.Sprintf("%02d.%03d.%03d/%04d-%02d",
|
|
rng.Intn(99), rng.Intn(999), rng.Intn(999),
|
|
rng.Intn(9999)+1, rng.Intn(99))
|
|
}
|