- Updated SeedLean and SeedFull CREATE TABLE statements - Added new fields to INSERT statements - Updated generateTenants to include phone (random), operating_hours, is_24_hours - Fixes 404 on /api/v1/companies/me due to missing columns
635 lines
21 KiB
Go
635 lines
21 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"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// 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",
|
|
}
|
|
|
|
// Seed dispatches based on mode
|
|
func Seed(dsn, mode string) (string, error) {
|
|
if mode == "lean" {
|
|
return SeedLean(dsn)
|
|
}
|
|
return SeedFull(dsn)
|
|
}
|
|
|
|
func SeedLean(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()
|
|
log.Println("🧹 [Lean] Resetting database...")
|
|
|
|
// Re-create tables
|
|
mustExec(db, `DROP TABLE IF EXISTS shipments CASCADE`)
|
|
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`)
|
|
|
|
// Create tables (Schema must match backend migrations!)
|
|
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 '',
|
|
phone TEXT NOT NULL DEFAULT '',
|
|
operating_hours TEXT NOT NULL DEFAULT '',
|
|
is_24_hours BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE users (
|
|
id UUID PRIMARY KEY,
|
|
company_id UUID NOT NULL REFERENCES companies(id),
|
|
role TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
username TEXT NOT NULL UNIQUE,
|
|
email TEXT NOT NULL UNIQUE,
|
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
password_hash TEXT NOT NULL,
|
|
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
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE orders (
|
|
id UUID PRIMARY KEY,
|
|
buyer_id UUID NOT NULL REFERENCES companies(id),
|
|
seller_id UUID NOT NULL REFERENCES companies(id),
|
|
status TEXT NOT NULL,
|
|
total_cents BIGINT NOT NULL,
|
|
shipping_recipient_name TEXT,
|
|
shipping_street TEXT,
|
|
shipping_number TEXT,
|
|
shipping_complement TEXT,
|
|
shipping_district TEXT,
|
|
shipping_city TEXT,
|
|
shipping_state TEXT,
|
|
shipping_zip_code TEXT,
|
|
shipping_country TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE order_items (
|
|
id UUID PRIMARY KEY,
|
|
order_id UUID NOT NULL REFERENCES orders(id),
|
|
product_id UUID NOT NULL REFERENCES products(id),
|
|
quantity BIGINT NOT NULL,
|
|
unit_cents BIGINT NOT NULL,
|
|
batch TEXT NOT NULL,
|
|
expires_at DATE NOT NULL
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE cart_items (
|
|
id UUID PRIMARY KEY,
|
|
buyer_id UUID NOT NULL REFERENCES companies(id),
|
|
product_id UUID NOT NULL REFERENCES products(id),
|
|
quantity BIGINT NOT NULL,
|
|
unit_cents BIGINT NOT NULL,
|
|
batch TEXT,
|
|
expires_at DATE,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL,
|
|
UNIQUE (buyer_id, product_id)
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE reviews (
|
|
id UUID PRIMARY KEY,
|
|
order_id UUID NOT NULL UNIQUE REFERENCES orders(id),
|
|
buyer_id UUID NOT NULL REFERENCES companies(id),
|
|
seller_id UUID NOT NULL REFERENCES companies(id),
|
|
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
|
comment TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE shipments (
|
|
id UUID PRIMARY KEY,
|
|
order_id UUID NOT NULL UNIQUE REFERENCES orders(id),
|
|
carrier TEXT NOT NULL,
|
|
tracking_code TEXT,
|
|
external_tracking TEXT,
|
|
status TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL
|
|
)`)
|
|
|
|
// Helper for hashing
|
|
hashPwd := func(pwd string) string {
|
|
h, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
|
return string(h)
|
|
}
|
|
defaultPwdHash := hashPwd("123456")
|
|
|
|
// Fixed Pharmacies Data
|
|
pharmacies := []struct {
|
|
Name string
|
|
CNPJ string
|
|
Lat float64
|
|
Lng float64
|
|
Suffix string // for usernames/emails e.g. "1" -> dono1, email1
|
|
}{
|
|
{
|
|
Name: "Farmácia Central",
|
|
CNPJ: "11111111000111",
|
|
Lat: AnapolisLat,
|
|
Lng: AnapolisLng,
|
|
Suffix: "1",
|
|
},
|
|
{
|
|
Name: "Farmácia Jundiaí",
|
|
CNPJ: "22222222000122",
|
|
Lat: AnapolisLat + 0.015, // Slightly North
|
|
Lng: AnapolisLng + 0.010, // East
|
|
Suffix: "2",
|
|
},
|
|
{
|
|
Name: "Farmácia Jaiara",
|
|
CNPJ: "33333333000133",
|
|
Lat: AnapolisLat + 0.030, // More North
|
|
Lng: AnapolisLng - 0.010, // West
|
|
Suffix: "3",
|
|
},
|
|
{
|
|
Name: "Farmácia Universitária",
|
|
CNPJ: "44444444000144",
|
|
Lat: AnapolisLat - 0.020, // South
|
|
Lng: AnapolisLng + 0.020, // East
|
|
Suffix: "4",
|
|
},
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
createdUsers := []string{}
|
|
var allProducts []map[string]interface{}
|
|
var pharmacyCompanyIDs []uuid.UUID
|
|
|
|
for _, ph := range pharmacies {
|
|
// 1. Create Company
|
|
companyID := uuid.Must(uuid.NewV7())
|
|
_, err = db.ExecContext(ctx, `
|
|
INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, city, state, phone, operating_hours, is_24_hours, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`,
|
|
companyID, ph.CNPJ, ph.Name, "farmacia", fmt.Sprintf("CRF-GO-%s", ph.Suffix), true, ph.Lat, ph.Lng, "Anápolis", "GO", "(62) 99999-00"+ph.Suffix, "Seg-Sex: 08:00-18:00, Sáb: 08:00-12:00", false, now, now,
|
|
)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create library %s: %v", ph.Name, err)
|
|
}
|
|
|
|
// 2. Create Users (Dono, Colab, Entregador)
|
|
roles := []struct {
|
|
Role string
|
|
UserBase string
|
|
NameBase string
|
|
}{
|
|
{"Dono", "dono", "Dono"},
|
|
{"Colaborador", "colab", "Colaborador"},
|
|
{"Entregador", "entregador", "Entregador"},
|
|
}
|
|
|
|
for _, r := range roles {
|
|
uid := uuid.Must(uuid.NewV7())
|
|
username := fmt.Sprintf("%s%s", r.UserBase, ph.Suffix)
|
|
email := fmt.Sprintf("%s%s@saveinmed.com", r.UserBase, ph.Suffix)
|
|
name := fmt.Sprintf("%s %s", r.NameBase, ph.Name)
|
|
|
|
mustExec(db, fmt.Sprintf(`INSERT INTO users (id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at)
|
|
VALUES ('%s', '%s', '%s', '%s', '%s', '%s', true, '%s', NOW(), NOW())`,
|
|
uid, companyID, r.Role, name, username, email, defaultPwdHash,
|
|
))
|
|
createdUsers = append(createdUsers, fmt.Sprintf("%s (%s)", username, r.Role))
|
|
}
|
|
|
|
// 3. Create Products (20-50 products per pharmacy)
|
|
numProds := 20 + rng.Intn(31) // 20-50
|
|
prods := generateProducts(rng, companyID, numProds)
|
|
for _, p := range prods {
|
|
_, 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 lean: %v", err)
|
|
}
|
|
}
|
|
// Store products for orders/cart creation later
|
|
allProducts = append(allProducts, prods...)
|
|
pharmacyCompanyIDs = append(pharmacyCompanyIDs, companyID)
|
|
log.Printf("✅ [Lean] %s created with %d products", ph.Name, len(prods))
|
|
}
|
|
|
|
// 4. Create some orders between pharmacies
|
|
if len(allProducts) > 0 && len(pharmacyCompanyIDs) > 1 {
|
|
// Create 5-10 orders
|
|
numOrders := 5 + rng.Intn(6)
|
|
for i := 0; i < numOrders; i++ {
|
|
// Random buyer and seller (different companies)
|
|
buyerIdx := rng.Intn(len(pharmacyCompanyIDs))
|
|
sellerIdx := (buyerIdx + 1) % len(pharmacyCompanyIDs)
|
|
buyerID := pharmacyCompanyIDs[buyerIdx]
|
|
sellerID := pharmacyCompanyIDs[sellerIdx]
|
|
|
|
// Find products from seller
|
|
var sellerProducts []map[string]interface{}
|
|
for _, p := range allProducts {
|
|
if p["seller_id"].(uuid.UUID).String() == sellerID.String() {
|
|
sellerProducts = append(sellerProducts, p)
|
|
}
|
|
}
|
|
if len(sellerProducts) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Create order with 1-3 items
|
|
orderID := uuid.Must(uuid.NewV7())
|
|
statuses := []string{"Pendente", "Pago", "Faturado", "Entregue"}
|
|
status := statuses[rng.Intn(len(statuses))]
|
|
totalCents := int64(0)
|
|
|
|
mustExec(db, fmt.Sprintf(`INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, created_at, updated_at)
|
|
VALUES ('%s', '%s', '%s', '%s', %d, NOW(), NOW())`,
|
|
orderID, buyerID, sellerID, status, totalCents,
|
|
))
|
|
|
|
// Create order items
|
|
numItems := 1 + rng.Intn(3)
|
|
for j := 0; j < numItems && j < len(sellerProducts); j++ {
|
|
prod := sellerProducts[rng.Intn(len(sellerProducts))]
|
|
qty := int64(1 + rng.Intn(5))
|
|
itemID := uuid.Must(uuid.NewV7())
|
|
priceCents := prod["price_cents"].(int64)
|
|
totalCents += priceCents * qty
|
|
|
|
mustExec(db, fmt.Sprintf(`INSERT INTO order_items (id, order_id, product_id, quantity, unit_cents, batch, expires_at)
|
|
VALUES ('%s', '%s', '%s', %d, %d, '%s', '%s')`,
|
|
itemID, orderID, prod["id"].(uuid.UUID), qty, priceCents, prod["batch"].(string), prod["expires_at"].(time.Time).Format(time.RFC3339),
|
|
))
|
|
}
|
|
|
|
// Update total cents on order because it we calculated it inside the loop
|
|
mustExec(db, fmt.Sprintf(`UPDATE orders SET total_cents = %d WHERE id = '%s'`, totalCents, orderID))
|
|
|
|
// Create Review if Delivered
|
|
if status == "Entregue" {
|
|
rating := 3 + rng.Intn(3) // 3-5 stars
|
|
comment := []string{"Ótimo atendimento!", "Entrega rápida", "Recomendo", "Tudo certo"}[rng.Intn(4)]
|
|
mustExec(db, fmt.Sprintf(`INSERT INTO reviews (id, order_id, buyer_id, seller_id, rating, comment, created_at)
|
|
VALUES ('%s', '%s', '%s', '%s', %d, '%s', NOW())`,
|
|
uuid.Must(uuid.NewV7()), orderID, buyerID, sellerID, rating, comment,
|
|
))
|
|
}
|
|
|
|
// Create Shipment if Faturado or Entregue
|
|
if status == "Faturado" || status == "Entregue" {
|
|
carrier := []string{"Correios", "Loggi", "TotalExpress"}[rng.Intn(3)]
|
|
tracking := fmt.Sprintf("TRK-%d%d", rng.Intn(1000), rng.Intn(1000))
|
|
mustExec(db, fmt.Sprintf(`INSERT INTO shipments (id, order_id, carrier, tracking_code, external_tracking, status, created_at, updated_at)
|
|
VALUES ('%s', '%s', '%s', '%s', '', 'Enviado', NOW(), NOW())`,
|
|
uuid.Must(uuid.NewV7()), orderID, carrier, tracking,
|
|
))
|
|
}
|
|
}
|
|
log.Printf("✅ [Lean] Created %d orders", numOrders)
|
|
|
|
// 5. Add cart items for first pharmacy
|
|
if len(pharmacyCompanyIDs) > 0 && len(allProducts) > 3 {
|
|
buyerID := pharmacyCompanyIDs[0]
|
|
for i := 0; i < 3; i++ {
|
|
prod := allProducts[rng.Intn(len(allProducts))]
|
|
if prod["seller_id"].(uuid.UUID).String() == buyerID.String() {
|
|
continue // Skip own products
|
|
}
|
|
cartItemID := uuid.Must(uuid.NewV7())
|
|
qty := int64(1 + rng.Intn(3))
|
|
priceCents := prod["price_cents"].(int64)
|
|
mustExec(db, fmt.Sprintf(`INSERT INTO cart_items (id, buyer_id, product_id, quantity, unit_cents, created_at, updated_at)
|
|
VALUES ('%s', '%s', '%s', %d, %d, NOW(), NOW()) ON CONFLICT DO NOTHING`,
|
|
cartItemID, buyerID, prod["id"].(uuid.UUID), qty, priceCents,
|
|
))
|
|
}
|
|
log.Printf("✅ [Lean] Added cart items")
|
|
}
|
|
}
|
|
|
|
// 6. Create Global Admin (linked to first pharmacy for FK constraint)
|
|
var centralID string
|
|
err = db.Get(¢ralID, "SELECT id FROM companies WHERE cnpj = '11111111000111'")
|
|
if err == nil {
|
|
adminID := uuid.Must(uuid.NewV7())
|
|
mustExec(db, fmt.Sprintf(`INSERT INTO users (id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at)
|
|
VALUES ('%s', '%s', 'Admin', 'Administrador Global', 'admin', 'admin@saveinmed.com', true, '%s', NOW(), NOW())`,
|
|
adminID, centralID, hashPwd("admin123"),
|
|
))
|
|
createdUsers = append(createdUsers, "admin (Admin)")
|
|
}
|
|
|
|
return fmt.Sprintf("Lean seed completed. 4 Pharmacies. Users: %d. Pass: 123456 (admin: admin123)", len(createdUsers)), nil
|
|
}
|
|
|
|
func SeedFull(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 '',
|
|
phone TEXT NOT NULL DEFAULT '',
|
|
operating_hours TEXT NOT NULL DEFAULT '',
|
|
is_24_hours BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL
|
|
)`)
|
|
|
|
mustExec(db, `CREATE TABLE users (
|
|
id UUID PRIMARY KEY,
|
|
company_id UUID NOT NULL REFERENCES companies(id),
|
|
role TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
username TEXT NOT NULL UNIQUE,
|
|
email TEXT NOT NULL UNIQUE,
|
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
password_hash TEXT NOT NULL,
|
|
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, phone, operating_hours, is_24_hours, created_at, updated_at)
|
|
VALUES (:id, :cnpj, :corporate_name, :category, :license_number, :is_verified, :latitude, :longitude, :city, :state, :phone, :operating_hours, :is_24_hours, :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",
|
|
"phone": fmt.Sprintf("(62) 9%04d-%04d", rng.Intn(9999), rng.Intn(9999)),
|
|
"operating_hours": "Seg-Sex: 08:00-18:00",
|
|
"is_24_hours": rng.Float32() < 0.1, // 10% are 24h
|
|
"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
|
|
}
|
|
}
|