fix(backend): fix build errors, update tests, and improve documentation
- Add GetUserByEmail to Repository interface for password reset flow - Add username to UpdateUser query - Fix config_test.go: remove references to deleted DB pool fields - Fix handler_test.go: add GetUserByUsername to MockRepository - Fix usecase_test.go: add GetUserByUsername and update auth tests - Update backend README with auth and admin seeding info - Create seeder-api README with usage and warnings
This commit is contained in:
parent
4612172b3c
commit
9997aed18a
7 changed files with 215 additions and 43 deletions
|
|
@ -83,8 +83,41 @@ backend/
|
||||||
### Variáveis de Ambiente
|
### Variáveis de Ambiente
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Banco de Dados
|
||||||
DATABASE_URL=postgres://user:password@localhost:5432/saveinmed?sslmode=disable
|
DATABASE_URL=postgres://user:password@localhost:5432/saveinmed?sslmode=disable
|
||||||
PORT=8080
|
|
||||||
|
# Servidor
|
||||||
|
BACKEND_PORT=8214
|
||||||
|
|
||||||
|
# Autenticação
|
||||||
|
JWT_SECRET=your-secret-key
|
||||||
|
JWT_EXPIRES_IN=24h
|
||||||
|
PASSWORD_PEPPER=your-pepper
|
||||||
|
|
||||||
|
# Admin Seeding (criado automaticamente na inicialização)
|
||||||
|
ADMIN_NAME=Administrator
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_EMAIL=admin@saveinmed.com
|
||||||
|
ADMIN_PASSWORD=admin123
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
CORS_ORIGINS=*
|
||||||
|
|
||||||
|
# Mercado Pago
|
||||||
|
MERCADOPAGO_BASE_URL=https://api.mercadopago.com
|
||||||
|
MARKETPLACE_COMMISSION=2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Autenticação
|
||||||
|
|
||||||
|
A autenticação utiliza **username** (não email) para login:
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /api/v1/auth/login
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin123"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pré-requisitos
|
### Pré-requisitos
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,9 @@ import (
|
||||||
func TestLoadDefaults(t *testing.T) {
|
func TestLoadDefaults(t *testing.T) {
|
||||||
// Clear any environment variables that might interfere
|
// Clear any environment variables that might interfere
|
||||||
envVars := []string{
|
envVars := []string{
|
||||||
"APP_NAME", "BACKEND_PORT", "DATABASE_URL", "DB_MAX_OPEN_CONNS",
|
"APP_NAME", "BACKEND_PORT", "DATABASE_URL",
|
||||||
"DB_MAX_IDLE_CONNS", "DB_CONN_MAX_IDLE", "MERCADOPAGO_BASE_URL",
|
"MERCADOPAGO_BASE_URL", "MARKETPLACE_COMMISSION", "JWT_SECRET", "JWT_EXPIRES_IN",
|
||||||
"MARKETPLACE_COMMISSION", "JWT_SECRET", "JWT_EXPIRES_IN",
|
"PASSWORD_PEPPER", "CORS_ORIGINS", "ADMIN_NAME", "ADMIN_USERNAME", "ADMIN_EMAIL", "ADMIN_PASSWORD",
|
||||||
"PASSWORD_PEPPER", "CORS_ORIGINS",
|
|
||||||
}
|
}
|
||||||
origEnvs := make(map[string]string)
|
origEnvs := make(map[string]string)
|
||||||
for _, key := range envVars {
|
for _, key := range envVars {
|
||||||
|
|
@ -35,15 +34,6 @@ func TestLoadDefaults(t *testing.T) {
|
||||||
if cfg.Port != "8214" {
|
if cfg.Port != "8214" {
|
||||||
t.Errorf("expected Port '8214', got '%s'", cfg.Port)
|
t.Errorf("expected Port '8214', got '%s'", cfg.Port)
|
||||||
}
|
}
|
||||||
if cfg.MaxOpenConns != 15 {
|
|
||||||
t.Errorf("expected MaxOpenConns 15, got %d", cfg.MaxOpenConns)
|
|
||||||
}
|
|
||||||
if cfg.MaxIdleConns != 5 {
|
|
||||||
t.Errorf("expected MaxIdleConns 5, got %d", cfg.MaxIdleConns)
|
|
||||||
}
|
|
||||||
if cfg.ConnMaxIdle != 5*time.Minute {
|
|
||||||
t.Errorf("expected ConnMaxIdle 5m, got %v", cfg.ConnMaxIdle)
|
|
||||||
}
|
|
||||||
if cfg.JWTSecret != "dev-secret" {
|
if cfg.JWTSecret != "dev-secret" {
|
||||||
t.Errorf("expected JWTSecret 'dev-secret', got '%s'", cfg.JWTSecret)
|
t.Errorf("expected JWTSecret 'dev-secret', got '%s'", cfg.JWTSecret)
|
||||||
}
|
}
|
||||||
|
|
@ -56,33 +46,47 @@ func TestLoadDefaults(t *testing.T) {
|
||||||
if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "*" {
|
if len(cfg.CORSOrigins) != 1 || cfg.CORSOrigins[0] != "*" {
|
||||||
t.Errorf("expected CORSOrigins ['*'], got %v", cfg.CORSOrigins)
|
t.Errorf("expected CORSOrigins ['*'], got %v", cfg.CORSOrigins)
|
||||||
}
|
}
|
||||||
|
if cfg.AdminName != "Administrator" {
|
||||||
|
t.Errorf("expected AdminName 'Administrator', got '%s'", cfg.AdminName)
|
||||||
|
}
|
||||||
|
if cfg.AdminUsername != "admin" {
|
||||||
|
t.Errorf("expected AdminUsername 'admin', got '%s'", cfg.AdminUsername)
|
||||||
|
}
|
||||||
|
if cfg.AdminEmail != "admin@saveinmed.com" {
|
||||||
|
t.Errorf("expected AdminEmail 'admin@saveinmed.com', got '%s'", cfg.AdminEmail)
|
||||||
|
}
|
||||||
|
if cfg.AdminPassword != "admin123" {
|
||||||
|
t.Errorf("expected AdminPassword 'admin123', got '%s'", cfg.AdminPassword)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadFromEnv(t *testing.T) {
|
func TestLoadFromEnv(t *testing.T) {
|
||||||
os.Setenv("APP_NAME", "test-app")
|
os.Setenv("APP_NAME", "test-app")
|
||||||
os.Setenv("BACKEND_PORT", "9999")
|
os.Setenv("BACKEND_PORT", "9999")
|
||||||
os.Setenv("DATABASE_URL", "postgres://test:test@localhost:5432/test")
|
os.Setenv("DATABASE_URL", "postgres://test:test@localhost:5432/test")
|
||||||
os.Setenv("DB_MAX_OPEN_CONNS", "100")
|
|
||||||
os.Setenv("DB_MAX_IDLE_CONNS", "50")
|
|
||||||
os.Setenv("DB_CONN_MAX_IDLE", "10m")
|
|
||||||
os.Setenv("MARKETPLACE_COMMISSION", "5.0")
|
os.Setenv("MARKETPLACE_COMMISSION", "5.0")
|
||||||
os.Setenv("JWT_SECRET", "super-secret")
|
os.Setenv("JWT_SECRET", "super-secret")
|
||||||
os.Setenv("JWT_EXPIRES_IN", "12h")
|
os.Setenv("JWT_EXPIRES_IN", "12h")
|
||||||
os.Setenv("PASSWORD_PEPPER", "pepper123")
|
os.Setenv("PASSWORD_PEPPER", "pepper123")
|
||||||
os.Setenv("CORS_ORIGINS", "https://example.com,https://app.example.com")
|
os.Setenv("CORS_ORIGINS", "https://example.com,https://app.example.com")
|
||||||
|
os.Setenv("ADMIN_NAME", "CustomAdmin")
|
||||||
|
os.Setenv("ADMIN_USERNAME", "customadmin")
|
||||||
|
os.Setenv("ADMIN_EMAIL", "custom@example.com")
|
||||||
|
os.Setenv("ADMIN_PASSWORD", "securepass")
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
os.Unsetenv("APP_NAME")
|
os.Unsetenv("APP_NAME")
|
||||||
os.Unsetenv("BACKEND_PORT")
|
os.Unsetenv("BACKEND_PORT")
|
||||||
os.Unsetenv("DATABASE_URL")
|
os.Unsetenv("DATABASE_URL")
|
||||||
os.Unsetenv("DB_MAX_OPEN_CONNS")
|
|
||||||
os.Unsetenv("DB_MAX_IDLE_CONNS")
|
|
||||||
os.Unsetenv("DB_CONN_MAX_IDLE")
|
|
||||||
os.Unsetenv("MARKETPLACE_COMMISSION")
|
os.Unsetenv("MARKETPLACE_COMMISSION")
|
||||||
os.Unsetenv("JWT_SECRET")
|
os.Unsetenv("JWT_SECRET")
|
||||||
os.Unsetenv("JWT_EXPIRES_IN")
|
os.Unsetenv("JWT_EXPIRES_IN")
|
||||||
os.Unsetenv("PASSWORD_PEPPER")
|
os.Unsetenv("PASSWORD_PEPPER")
|
||||||
os.Unsetenv("CORS_ORIGINS")
|
os.Unsetenv("CORS_ORIGINS")
|
||||||
|
os.Unsetenv("ADMIN_NAME")
|
||||||
|
os.Unsetenv("ADMIN_USERNAME")
|
||||||
|
os.Unsetenv("ADMIN_EMAIL")
|
||||||
|
os.Unsetenv("ADMIN_PASSWORD")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cfg := Load()
|
cfg := Load()
|
||||||
|
|
@ -96,15 +100,6 @@ func TestLoadFromEnv(t *testing.T) {
|
||||||
if cfg.DatabaseURL != "postgres://test:test@localhost:5432/test" {
|
if cfg.DatabaseURL != "postgres://test:test@localhost:5432/test" {
|
||||||
t.Errorf("expected custom DatabaseURL, got '%s'", cfg.DatabaseURL)
|
t.Errorf("expected custom DatabaseURL, got '%s'", cfg.DatabaseURL)
|
||||||
}
|
}
|
||||||
if cfg.MaxOpenConns != 100 {
|
|
||||||
t.Errorf("expected MaxOpenConns 100, got %d", cfg.MaxOpenConns)
|
|
||||||
}
|
|
||||||
if cfg.MaxIdleConns != 50 {
|
|
||||||
t.Errorf("expected MaxIdleConns 50, got %d", cfg.MaxIdleConns)
|
|
||||||
}
|
|
||||||
if cfg.ConnMaxIdle != 10*time.Minute {
|
|
||||||
t.Errorf("expected ConnMaxIdle 10m, got %v", cfg.ConnMaxIdle)
|
|
||||||
}
|
|
||||||
if cfg.MarketplaceCommission != 5.0 {
|
if cfg.MarketplaceCommission != 5.0 {
|
||||||
t.Errorf("expected MarketplaceCommission 5.0, got %f", cfg.MarketplaceCommission)
|
t.Errorf("expected MarketplaceCommission 5.0, got %f", cfg.MarketplaceCommission)
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +115,18 @@ func TestLoadFromEnv(t *testing.T) {
|
||||||
if len(cfg.CORSOrigins) != 2 {
|
if len(cfg.CORSOrigins) != 2 {
|
||||||
t.Errorf("expected 2 CORS origins, got %d", len(cfg.CORSOrigins))
|
t.Errorf("expected 2 CORS origins, got %d", len(cfg.CORSOrigins))
|
||||||
}
|
}
|
||||||
|
if cfg.AdminName != "CustomAdmin" {
|
||||||
|
t.Errorf("expected AdminName 'CustomAdmin', got '%s'", cfg.AdminName)
|
||||||
|
}
|
||||||
|
if cfg.AdminUsername != "customadmin" {
|
||||||
|
t.Errorf("expected AdminUsername 'customadmin', got '%s'", cfg.AdminUsername)
|
||||||
|
}
|
||||||
|
if cfg.AdminEmail != "custom@example.com" {
|
||||||
|
t.Errorf("expected AdminEmail 'custom@example.com', got '%s'", cfg.AdminEmail)
|
||||||
|
}
|
||||||
|
if cfg.AdminPassword != "securepass" {
|
||||||
|
t.Errorf("expected AdminPassword 'securepass', got '%s'", cfg.AdminPassword)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddr(t *testing.T) {
|
func TestAddr(t *testing.T) {
|
||||||
|
|
@ -131,28 +138,18 @@ func TestAddr(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInvalidEnvValues(t *testing.T) {
|
func TestInvalidEnvValues(t *testing.T) {
|
||||||
os.Setenv("DB_MAX_OPEN_CONNS", "not-a-number")
|
|
||||||
os.Setenv("MARKETPLACE_COMMISSION", "invalid")
|
os.Setenv("MARKETPLACE_COMMISSION", "invalid")
|
||||||
os.Setenv("DB_CONN_MAX_IDLE", "bad-duration")
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
os.Unsetenv("DB_MAX_OPEN_CONNS")
|
|
||||||
os.Unsetenv("MARKETPLACE_COMMISSION")
|
os.Unsetenv("MARKETPLACE_COMMISSION")
|
||||||
os.Unsetenv("DB_CONN_MAX_IDLE")
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cfg := Load()
|
cfg := Load()
|
||||||
|
|
||||||
// Should use defaults when values are invalid
|
// Should use defaults when values are invalid
|
||||||
if cfg.MaxOpenConns != 15 {
|
|
||||||
t.Errorf("expected fallback MaxOpenConns 15, got %d", cfg.MaxOpenConns)
|
|
||||||
}
|
|
||||||
if cfg.MarketplaceCommission != 2.5 {
|
if cfg.MarketplaceCommission != 2.5 {
|
||||||
t.Errorf("expected fallback MarketplaceCommission 2.5, got %f", cfg.MarketplaceCommission)
|
t.Errorf("expected fallback MarketplaceCommission 2.5, got %f", cfg.MarketplaceCommission)
|
||||||
}
|
}
|
||||||
if cfg.ConnMaxIdle != 5*time.Minute {
|
|
||||||
t.Errorf("expected fallback ConnMaxIdle 5m, got %v", cfg.ConnMaxIdle)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmptyCORSOrigins(t *testing.T) {
|
func TestEmptyCORSOrigins(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,15 @@ func (m *MockRepository) GetUser(ctx context.Context, id uuid.UUID) (*domain.Use
|
||||||
return nil, errors.New("user not found")
|
return nil, errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockRepository) GetUserByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||||
|
for _, u := range m.users {
|
||||||
|
if u.Username == username {
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
for _, u := range m.users {
|
for _, u := range m.users {
|
||||||
if u.Email == email {
|
if u.Email == email {
|
||||||
|
|
@ -350,7 +359,7 @@ func TestCreateProduct(t *testing.T) {
|
||||||
func TestLoginInvalidCredentials(t *testing.T) {
|
func TestLoginInvalidCredentials(t *testing.T) {
|
||||||
h := newTestHandler()
|
h := newTestHandler()
|
||||||
|
|
||||||
payload := `{"email":"nonexistent@test.com","password":"wrongpassword"}`
|
payload := `{"username":"nonexistent","password":"wrongpassword"}`
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader([]byte(payload)))
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader([]byte(payload)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
|
||||||
|
|
@ -842,11 +842,20 @@ func (r *Repository) GetUserByUsername(ctx context.Context, username string) (*d
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
var user domain.User
|
||||||
|
query := `SELECT id, company_id, role, name, username, email, email_verified, password_hash, created_at, updated_at FROM users WHERE email = $1`
|
||||||
|
if err := r.db.GetContext(ctx, &user, query, email); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) UpdateUser(ctx context.Context, user *domain.User) error {
|
func (r *Repository) UpdateUser(ctx context.Context, user *domain.User) error {
|
||||||
user.UpdatedAt = time.Now().UTC()
|
user.UpdatedAt = time.Now().UTC()
|
||||||
|
|
||||||
query := `UPDATE users
|
query := `UPDATE users
|
||||||
SET company_id = :company_id, role = :role, name = :name, email = :email, email_verified = :email_verified, password_hash = :password_hash, updated_at = :updated_at
|
SET company_id = :company_id, role = :role, name = :name, username = :username, email = :email, email_verified = :email_verified, password_hash = :password_hash, updated_at = :updated_at
|
||||||
WHERE id = :id`
|
WHERE id = :id`
|
||||||
|
|
||||||
res, err := r.db.NamedExecContext(ctx, query, user)
|
res, err := r.db.NamedExecContext(ctx, query, user)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ type Repository interface {
|
||||||
ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error)
|
ListUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error)
|
||||||
GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error)
|
GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error)
|
||||||
GetUserByUsername(ctx context.Context, username string) (*domain.User, error)
|
GetUserByUsername(ctx context.Context, username string) (*domain.User, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
UpdateUser(ctx context.Context, user *domain.User) error
|
UpdateUser(ctx context.Context, user *domain.User) error
|
||||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,15 @@ func (m *MockRepository) GetUser(ctx context.Context, id uuid.UUID) (*domain.Use
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockRepository) GetUserByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||||
|
for _, u := range m.users {
|
||||||
|
if u.Username == username {
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (m *MockRepository) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
for _, u := range m.users {
|
for _, u := range m.users {
|
||||||
if u.Email == email {
|
if u.Email == email {
|
||||||
|
|
@ -694,6 +703,7 @@ func TestAuthenticate(t *testing.T) {
|
||||||
CompanyID: uuid.Must(uuid.NewV4()),
|
CompanyID: uuid.Must(uuid.NewV4()),
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
Name: "Test User",
|
Name: "Test User",
|
||||||
|
Username: "authuser",
|
||||||
Email: "auth@example.com",
|
Email: "auth@example.com",
|
||||||
}
|
}
|
||||||
err := svc.CreateUser(ctx, user, "testpass123")
|
err := svc.CreateUser(ctx, user, "testpass123")
|
||||||
|
|
@ -705,7 +715,7 @@ func TestAuthenticate(t *testing.T) {
|
||||||
repo.users[0] = *user
|
repo.users[0] = *user
|
||||||
|
|
||||||
// Test authentication
|
// Test authentication
|
||||||
token, expiresAt, err := svc.Authenticate(ctx, "auth@example.com", "testpass123")
|
token, expiresAt, err := svc.Authenticate(ctx, "authuser", "testpass123")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to authenticate: %v", err)
|
t.Fatalf("failed to authenticate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -726,12 +736,13 @@ func TestAuthenticateInvalidPassword(t *testing.T) {
|
||||||
CompanyID: uuid.Must(uuid.NewV4()),
|
CompanyID: uuid.Must(uuid.NewV4()),
|
||||||
Role: "admin",
|
Role: "admin",
|
||||||
Name: "Test User",
|
Name: "Test User",
|
||||||
|
Username: "failuser",
|
||||||
Email: "fail@example.com",
|
Email: "fail@example.com",
|
||||||
}
|
}
|
||||||
svc.CreateUser(ctx, user, "correctpass")
|
svc.CreateUser(ctx, user, "correctpass")
|
||||||
repo.users[0] = *user
|
repo.users[0] = *user
|
||||||
|
|
||||||
_, _, err := svc.Authenticate(ctx, "fail@example.com", "wrongpass")
|
_, _, err := svc.Authenticate(ctx, "failuser", "wrongpass")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected authentication to fail")
|
t.Error("expected authentication to fail")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
112
seeder-api/README.md
Normal file
112
seeder-api/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# SaveInMed Seeder API
|
||||||
|
|
||||||
|
Microserviço utilitário para popular o banco de dados com dados de teste para desenvolvimento e demonstração.
|
||||||
|
|
||||||
|
## ⚠️ AVISO IMPORTANTE
|
||||||
|
|
||||||
|
**Este serviço é DESTRUTIVO!** Ele:
|
||||||
|
1. **REMOVE** todas as tabelas existentes (`companies`, `products`, `users`, etc.)
|
||||||
|
2. **RECRIA** apenas as tabelas `companies` e `products`
|
||||||
|
3. **NÃO RECRIA** a tabela `users` - você precisa reiniciar o backend após usar o seeder
|
||||||
|
|
||||||
|
## 🎯 Propósito
|
||||||
|
|
||||||
|
Gerar dados de teste para o marketplace SaveInMed, criando:
|
||||||
|
- **400 farmácias** na região de Anápolis/GO
|
||||||
|
- **20-500 produtos** por farmácia
|
||||||
|
- Dados variados de medicamentos com preços e validades realistas
|
||||||
|
|
||||||
|
## 🏗️ Arquitetura
|
||||||
|
|
||||||
|
```
|
||||||
|
seeder-api/
|
||||||
|
├── main.go # Entry point HTTP (POST /seed)
|
||||||
|
├── pkg/
|
||||||
|
│ └── seeder/
|
||||||
|
│ └── seeder.go # Lógica de geração de dados
|
||||||
|
├── go.mod
|
||||||
|
└── go.sum
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📍 Dados Gerados
|
||||||
|
|
||||||
|
### Localização
|
||||||
|
- Centro em **Anápolis, GO** (Lat: -16.3281, Lng: -48.9530)
|
||||||
|
- Variação de ~5km para cada farmácia
|
||||||
|
|
||||||
|
### Farmácias
|
||||||
|
- **Quantidade**: 400 empresas
|
||||||
|
- **Categoria**: `farmacia`
|
||||||
|
- **70%** verificadas (`is_verified = true`)
|
||||||
|
- CNPJs gerados automaticamente
|
||||||
|
|
||||||
|
### Produtos (Medicamentos)
|
||||||
|
- **20-500 produtos** por farmácia
|
||||||
|
- **Categorias**: Analgésicos, Antibióticos, Anti-inflamatórios, Cardiovasculares, Dermatológicos, Vitaminas, Oftálmicos, Respiratórios, etc.
|
||||||
|
- **Validade**: 30 dias a 2 anos
|
||||||
|
- **Variação de preço**: -20% a +30% do preço base
|
||||||
|
|
||||||
|
## 🔧 Variáveis de Ambiente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATABASE_URL=postgres://user:password@host:port/dbname?sslmode=disable
|
||||||
|
PORT=8216 # Porta padrão do seeder
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Uso
|
||||||
|
|
||||||
|
### Executar Localmente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configurar DATABASE_URL
|
||||||
|
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/saveinmed?sslmode=disable
|
||||||
|
|
||||||
|
# Executar
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Iniciar seeding
|
||||||
|
POST http://localhost:8216/seed
|
||||||
|
|
||||||
|
# Resposta de sucesso
|
||||||
|
{
|
||||||
|
"tenants": 400,
|
||||||
|
"products": 85432,
|
||||||
|
"location": "Anápolis, GO"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚡ Fluxo de Uso Recomendado
|
||||||
|
|
||||||
|
1. **Parar o backend principal** (para evitar conflitos de conexão)
|
||||||
|
2. **Executar o seeder**: `curl -X POST http://localhost:8216/seed`
|
||||||
|
3. **Reiniciar o backend** (para aplicar migrations e recriar tabela `users`)
|
||||||
|
4. A API estará pronta com dados de teste
|
||||||
|
|
||||||
|
## 🐳 Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
docker build -t saveinmed-seeder:latest .
|
||||||
|
|
||||||
|
# Run
|
||||||
|
docker run -p 8216:8216 \
|
||||||
|
-e DATABASE_URL=postgres://user:password@host:5432/saveinmed \
|
||||||
|
saveinmed-seeder:latest
|
||||||
|
|
||||||
|
# Seed
|
||||||
|
curl -X POST http://localhost:8216/seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Notas
|
||||||
|
|
||||||
|
- Os dados são **regeneráveis** - execute novamente para limpar e recriar
|
||||||
|
- Ideal para ambientes de **desenvolvimento** e **staging**
|
||||||
|
- **NÃO USE EM PRODUÇÃO** - vai apagar todos os dados reais!
|
||||||
|
|
||||||
|
## 📝 Licença
|
||||||
|
|
||||||
|
MIT
|
||||||
Loading…
Reference in a new issue