381 lines
11 KiB
Go
381 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid/v5"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/jmoiron/sqlx"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/saveinmed/backend-go/internal/config"
|
|
"github.com/saveinmed/backend-go/internal/domain"
|
|
)
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
|
|
db, err := sqlx.Open("pgx", cfg.DatabaseURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to DB: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := db.Ping(); err != nil {
|
|
log.Fatalf("Failed to ping DB: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
log.Println("🧹 Cleaning database...")
|
|
cleanDB(ctx, db)
|
|
|
|
log.Println("🌱 Seeding data...")
|
|
seedData(ctx, db, cfg)
|
|
|
|
log.Println("✅ Seeding complete!")
|
|
}
|
|
|
|
func cleanDB(ctx context.Context, db *sqlx.DB) {
|
|
tables := []string{
|
|
"reviews", "shipments", "payment_preferences", "orders", "order_items",
|
|
"cart_items", "products", "companies", "users", "shipping_settings",
|
|
}
|
|
|
|
for _, table := range tables {
|
|
_, err := db.ExecContext(ctx, fmt.Sprintf("TRUNCATE TABLE %s CASCADE", table))
|
|
if err != nil {
|
|
// Ignore error if table doesn't exist or is empty
|
|
log.Printf("Warning cleaning %s: %v", table, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func seedData(ctx context.Context, db *sqlx.DB, cfg config.Config) {
|
|
// 1. Seed Admin
|
|
adminCompanyID := uuid.Must(uuid.NewV7())
|
|
createCompany(ctx, db, &domain.Company{
|
|
ID: adminCompanyID,
|
|
CNPJ: "00000000000000",
|
|
CorporateName: "SaveInMed Admin",
|
|
Category: "admin",
|
|
LicenseNumber: "ADMIN",
|
|
IsVerified: true,
|
|
})
|
|
createUser(ctx, db, &domain.User{
|
|
CompanyID: adminCompanyID,
|
|
Role: "Admin",
|
|
Name: cfg.AdminName,
|
|
Username: cfg.AdminUsername,
|
|
Email: cfg.AdminEmail,
|
|
}, cfg.AdminPassword, cfg.PasswordPepper)
|
|
|
|
// 2. Distributors (Sellers - SP Center)
|
|
distributor1ID := uuid.Must(uuid.NewV7())
|
|
createCompany(ctx, db, &domain.Company{
|
|
ID: distributor1ID,
|
|
CNPJ: "11111111000111",
|
|
CorporateName: "Distribuidora Nacional",
|
|
Category: "distribuidora",
|
|
LicenseNumber: "DIST-001",
|
|
IsVerified: true,
|
|
Latitude: -23.55052,
|
|
Longitude: -46.633308,
|
|
})
|
|
createUser(ctx, db, &domain.User{
|
|
CompanyID: distributor1ID,
|
|
Role: "Owner",
|
|
Name: "Dono da Distribuidora",
|
|
Username: "distribuidora",
|
|
Email: "distribuidora@saveinmed.com",
|
|
}, "123456", cfg.PasswordPepper)
|
|
createShippingSettings(ctx, db, distributor1ID, -23.55052, -46.633308, "Rua da Distribuidora, 1000")
|
|
|
|
// 2b. Second Distributor (Sellers - SP East Zone/Mooca)
|
|
distributor2ID := uuid.Must(uuid.NewV7())
|
|
createCompany(ctx, db, &domain.Company{
|
|
ID: distributor2ID,
|
|
CNPJ: "55555555000555",
|
|
CorporateName: "Distribuidora Leste Ltda",
|
|
Category: "distribuidora",
|
|
LicenseNumber: "DIST-002",
|
|
IsVerified: true,
|
|
Latitude: -23.55952,
|
|
Longitude: -46.593308,
|
|
})
|
|
createUser(ctx, db, &domain.User{
|
|
CompanyID: distributor2ID,
|
|
Role: "Owner",
|
|
Name: "Gerente Leste",
|
|
Username: "distribuidora_leste",
|
|
Email: "leste@saveinmed.com",
|
|
}, "123456", cfg.PasswordPepper)
|
|
createShippingSettings(ctx, db, distributor2ID, -23.55952, -46.593308, "Rua da Mooca, 500")
|
|
|
|
// 3. Pharmacies (Buyers)
|
|
pharmacy1ID := uuid.Must(uuid.NewV7())
|
|
createCompany(ctx, db, &domain.Company{
|
|
ID: pharmacy1ID,
|
|
CNPJ: "22222222000122",
|
|
CorporateName: "Farmácia Central",
|
|
Category: "farmacia",
|
|
LicenseNumber: "FARM-001",
|
|
IsVerified: true,
|
|
Latitude: -23.56052,
|
|
Longitude: -46.643308,
|
|
})
|
|
createUser(ctx, db, &domain.User{
|
|
CompanyID: pharmacy1ID,
|
|
Role: "Owner",
|
|
Name: "Dono da Farmácia",
|
|
Username: "farmacia",
|
|
Email: "farmacia@saveinmed.com",
|
|
}, "123456", cfg.PasswordPepper)
|
|
|
|
pharmacy2ID := uuid.Must(uuid.NewV7())
|
|
createCompany(ctx, db, &domain.Company{
|
|
ID: pharmacy2ID,
|
|
CNPJ: "33333333000133",
|
|
CorporateName: "Drogarias Tiete",
|
|
Category: "farmacia",
|
|
LicenseNumber: "FARM-002",
|
|
IsVerified: true,
|
|
Latitude: -23.51052,
|
|
Longitude: -46.613308,
|
|
})
|
|
createUser(ctx, db, &domain.User{
|
|
CompanyID: pharmacy2ID,
|
|
Role: "Owner",
|
|
Name: "Gerente Tiete",
|
|
Username: "farmacia2",
|
|
Email: "tiete@saveinmed.com",
|
|
}, "123456", cfg.PasswordPepper)
|
|
|
|
// 4. Products
|
|
// List of diverse products
|
|
commonMeds := []struct {
|
|
Name string
|
|
Price int64 // Base price
|
|
}{
|
|
{"Dipirona Sódica 500mg", 450},
|
|
{"Paracetamol 750mg", 600},
|
|
{"Ibuprofeno 400mg", 1100},
|
|
{"Amoxicilina 500mg", 2200},
|
|
{"Omeprazol 20mg", 1400},
|
|
{"Simeticona 40mg", 700},
|
|
{"Dorflex 36cp", 1850},
|
|
{"Neosaldina 30dg", 2100},
|
|
{"Torsilax 30cp", 1200},
|
|
{"Cimegripe 20caps", 1300},
|
|
{"Losartana Potássica 50mg", 800},
|
|
{"Atenolol 25mg", 950},
|
|
{"Metformina 850mg", 750},
|
|
{"Sildenafila 50mg", 1500},
|
|
{"Azitromicina 500mg", 2800},
|
|
}
|
|
|
|
var productIDs []uuid.UUID
|
|
|
|
// Seed products for Dist 1
|
|
for i, p := range commonMeds {
|
|
id := uuid.Must(uuid.NewV7())
|
|
expiry := time.Now().AddDate(1, 0, 0)
|
|
|
|
// Vary price slightly
|
|
finalPrice := p.Price + int64(i*10) - 50
|
|
if finalPrice < 100 {
|
|
finalPrice = 100
|
|
}
|
|
|
|
createProduct(ctx, db, &domain.Product{
|
|
ID: id,
|
|
SellerID: distributor1ID,
|
|
Name: p.Name,
|
|
Description: "Medicamento genérico de alta qualidade (Nacional)",
|
|
Batch: "BATCH-NAC-" + id.String()[:4],
|
|
ExpiresAt: expiry,
|
|
PriceCents: finalPrice,
|
|
Stock: 1000 + int64(i*100),
|
|
})
|
|
|
|
// Keep first 5 for orders
|
|
if i < 5 {
|
|
productIDs = append(productIDs, id)
|
|
}
|
|
}
|
|
|
|
// Seed products for Dist 2 (Leste) - Only some of them, different prices
|
|
for i, p := range commonMeds {
|
|
if i%2 == 0 {
|
|
continue
|
|
} // Skip half
|
|
|
|
id := uuid.Must(uuid.NewV7())
|
|
expiry := time.Now().AddDate(0, 6, 0) // Shorter expiry
|
|
|
|
// Cheaper but fewer stock
|
|
finalPrice := p.Price - 100
|
|
if finalPrice < 100 {
|
|
finalPrice = 100
|
|
}
|
|
|
|
createProduct(ctx, db, &domain.Product{
|
|
ID: id,
|
|
SellerID: distributor2ID,
|
|
Name: p.Name,
|
|
Description: "Distribuição exclusiva ZL",
|
|
Batch: "BATCH-ZL-" + id.String()[:4],
|
|
ExpiresAt: expiry,
|
|
PriceCents: finalPrice,
|
|
Stock: 50 + int64(i*10),
|
|
})
|
|
}
|
|
|
|
// 5. Orders
|
|
// Order 1: Pharmacy 1 buying from Dist 1
|
|
orderID := uuid.Must(uuid.NewV7())
|
|
totalCents := int64(0)
|
|
|
|
// Items
|
|
qty := int64(10)
|
|
priceItem := productIDs[0] // Dipirona from Dist 1
|
|
// We need to fetch price ideally but we know logic... let's just reuse base logic approx or fetch from struct above
|
|
// Simulating price:
|
|
unitPrice := commonMeds[0].Price - 50 // Same logic as above: p.Price + 0*10 - 50
|
|
|
|
itemTotal := unitPrice * qty
|
|
totalCents += itemTotal
|
|
|
|
createOrder(ctx, db, &domain.Order{
|
|
ID: orderID,
|
|
BuyerID: pharmacy1ID,
|
|
SellerID: distributor1ID,
|
|
Status: "Faturado", // Ready for "Confirmar Entrega" test
|
|
TotalCents: totalCents,
|
|
CreatedAt: time.Now().AddDate(0, 0, -2),
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
|
|
createOrderItem(ctx, db, &domain.OrderItem{
|
|
ID: uuid.Must(uuid.NewV7()),
|
|
OrderID: orderID,
|
|
ProductID: priceItem,
|
|
Quantity: qty,
|
|
UnitCents: unitPrice,
|
|
Batch: "BATCH-NAC-" + priceItem.String()[:4],
|
|
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
|
})
|
|
|
|
// Order 2: Pharmacy 2 buying from Dist 1 (Pending)
|
|
order2ID := uuid.Must(uuid.NewV7())
|
|
createOrder(ctx, db, &domain.Order{
|
|
ID: order2ID,
|
|
BuyerID: pharmacy2ID,
|
|
SellerID: distributor1ID,
|
|
Status: "Pendente",
|
|
TotalCents: 5000,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
})
|
|
|
|
}
|
|
|
|
func createCompany(ctx context.Context, db *sqlx.DB, c *domain.Company) {
|
|
now := time.Now().UTC()
|
|
c.CreatedAt = now
|
|
c.UpdatedAt = now
|
|
_, err := db.NamedExecContext(ctx, `
|
|
INSERT INTO companies (id, cnpj, corporate_name, category, license_number, is_verified, latitude, longitude, created_at, updated_at)
|
|
VALUES (:id, :cnpj, :corporate_name, :category, :license_number, :is_verified, :latitude, :longitude, :created_at, :updated_at)
|
|
`, c)
|
|
if err != nil {
|
|
log.Printf("Error creating company %s: %v", c.CorporateName, err)
|
|
}
|
|
}
|
|
|
|
func createUser(ctx context.Context, db *sqlx.DB, u *domain.User, password, pepper string) {
|
|
hashed, _ := bcrypt.GenerateFromPassword([]byte(password+pepper), bcrypt.DefaultCost)
|
|
u.ID = uuid.Must(uuid.NewV7())
|
|
u.PasswordHash = string(hashed)
|
|
u.CreatedAt = time.Now().UTC()
|
|
u.UpdatedAt = time.Now().UTC()
|
|
|
|
// Ensure email/username uniqueness is handled by DB constraint, usually we just insert
|
|
_, err := db.NamedExecContext(ctx, `
|
|
INSERT INTO users (id, company_id, role, name, username, email, password_hash, email_verified, created_at, updated_at)
|
|
VALUES (:id, :company_id, :role, :name, :username, :email, :password_hash, :email_verified, :created_at, :updated_at)
|
|
`, u)
|
|
if err != nil {
|
|
log.Printf("Error creating user %s: %v", u.Username, err)
|
|
}
|
|
}
|
|
|
|
func createProduct(ctx context.Context, db *sqlx.DB, p *domain.Product) {
|
|
now := time.Now().UTC()
|
|
p.CreatedAt = now
|
|
p.UpdatedAt = now
|
|
_, 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)
|
|
`, p)
|
|
if err != nil {
|
|
log.Printf("Error creating product %s: %v", p.Name, err)
|
|
}
|
|
}
|
|
|
|
func createShippingSettings(ctx context.Context, db *sqlx.DB, vendorID uuid.UUID, lat, lng float64, address string) {
|
|
now := time.Now().UTC()
|
|
settings := &domain.ShippingSettings{
|
|
VendorID: vendorID,
|
|
Active: true,
|
|
MaxRadiusKm: 50,
|
|
PricePerKmCents: 150, // R$ 1.50
|
|
MinFeeCents: 1000, // R$ 10.00
|
|
FreeShippingThresholdCents: nil,
|
|
PickupActive: true,
|
|
PickupAddress: address,
|
|
PickupHours: "Seg-Sex 08-18h",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Latitude: lat,
|
|
Longitude: lng,
|
|
}
|
|
|
|
_, err := db.NamedExecContext(ctx, `
|
|
INSERT INTO shipping_settings (
|
|
vendor_id, active, max_radius_km, price_per_km_cents, min_fee_cents,
|
|
free_shipping_threshold_cents, pickup_active, pickup_address, pickup_hours,
|
|
latitude, longitude, created_at, updated_at
|
|
) VALUES (
|
|
:vendor_id, :active, :max_radius_km, :price_per_km_cents, :min_fee_cents,
|
|
:free_shipping_threshold_cents, :pickup_active, :pickup_address, :pickup_hours,
|
|
:latitude, :longitude, :created_at, :updated_at
|
|
)
|
|
`, settings)
|
|
if err != nil {
|
|
log.Printf("Error creating shipping settings: %v", err)
|
|
}
|
|
}
|
|
|
|
func createOrder(ctx context.Context, db *sqlx.DB, o *domain.Order) {
|
|
_, err := db.NamedExecContext(ctx, `
|
|
INSERT INTO orders (id, buyer_id, seller_id, status, total_cents, created_at, updated_at)
|
|
VALUES (:id, :buyer_id, :seller_id, :status, :total_cents, :created_at, :updated_at)
|
|
`, o)
|
|
if err != nil {
|
|
log.Printf("Error creating order: %v", err)
|
|
}
|
|
}
|
|
|
|
func createOrderItem(ctx context.Context, db *sqlx.DB, item *domain.OrderItem) {
|
|
_, err := db.NamedExecContext(ctx, `
|
|
INSERT INTO order_items (id, order_id, product_id, quantity, unit_cents, batch, expires_at)
|
|
VALUES (:id, :order_id, :product_id, :quantity, :unit_cents, :batch, :expires_at)
|
|
`, item)
|
|
if err != nil {
|
|
log.Printf("Error creating order item: %v", err)
|
|
}
|
|
}
|