261 lines
8.5 KiB
Go
261 lines
8.5 KiB
Go
package seeder
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid/v5"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// 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 Seed(dsn string) (string, error) {
|
|
if dsn == "" {
|
|
return "", fmt.Errorf("DATABASE_URL not set")
|
|
}
|
|
|
|
db, err := sqlx.Connect("pgx", dsn)
|
|
if err != nil {
|
|
return "", fmt.Errorf("db connect: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
ctx := context.Background()
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
// Re-create tables to ensure schema match
|
|
log.Println("Re-creating tables...")
|
|
mustExec(db, `DROP TABLE IF EXISTS inventory_adjustments CASCADE`)
|
|
mustExec(db, `DROP TABLE IF EXISTS order_items CASCADE`)
|
|
mustExec(db, `DROP TABLE IF EXISTS orders CASCADE`)
|
|
mustExec(db, `DROP TABLE IF EXISTS cart_items CASCADE`)
|
|
mustExec(db, `DROP TABLE IF EXISTS reviews CASCADE`)
|
|
mustExec(db, `DROP TABLE IF EXISTS products CASCADE`)
|
|
mustExec(db, `DROP TABLE IF EXISTS users CASCADE`)
|
|
mustExec(db, `DROP TABLE IF EXISTS companies CASCADE`)
|
|
|
|
mustExec(db, `CREATE TABLE companies (
|
|
id UUID PRIMARY KEY,
|
|
cnpj TEXT NOT NULL UNIQUE,
|
|
corporate_name TEXT NOT NULL,
|
|
category TEXT NOT NULL DEFAULT 'farmacia',
|
|
license_number TEXT NOT NULL,
|
|
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
latitude DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
longitude DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
city TEXT NOT NULL DEFAULT '',
|
|
state TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE products (
|
|
id UUID PRIMARY KEY,
|
|
seller_id UUID NOT NULL REFERENCES companies(id),
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
batch TEXT NOT NULL,
|
|
expires_at DATE NOT NULL,
|
|
price_cents BIGINT NOT NULL,
|
|
stock BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL
|
|
)`)
|
|
|
|
// 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, description, expires_at, price_cents, stock, created_at, updated_at)
|
|
VALUES (:id, :seller_id, :name, :description, :batch, :description, :expires_at, :price_cents, :stock, :created_at, :updated_at)
|
|
ON CONFLICT DO NOTHING`, p)
|
|
if err != nil {
|
|
// Try again with minimal columns if the issue is named params mismatch
|
|
// Wait, I see duplicate description in named exec above?
|
|
// ORIGINAL:
|
|
// 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)
|
|
|
|
// I should verify the original file content.
|
|
// Line 152: INSERT INTO products (id, seller_id, name, description, batch, expires_at, price_cents, stock, created_at, updated_at)
|
|
// Line 153: VALUES (:id, :seller_id, :name, :description, :batch, :expires_at, :price_cents, :stock, :created_at, :updated_at)
|
|
|
|
// I will correct my manual typing in this block to match original.
|
|
|
|
_, 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, "", " ")
|
|
return string(out), nil
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
func mustExec(db *sqlx.DB, query string) {
|
|
_, err := db.Exec(query)
|
|
if err != nil {
|
|
log.Printf("exec %s: %v", query, err) // Don't fatal, just log
|
|
}
|
|
}
|