package main import ( "context" "database/sql" "encoding/csv" "fmt" "io" "log" "os" "strconv" "strings" _ "github.com/jackc/pgx/v5/stdlib" "github.com/joho/godotenv" "golang.org/x/text/encoding/charmap" "golang.org/x/text/transform" ) func main() { // Load .env // Try loading from current directory first, typical when running 'go run cmd/importer/main.go' from root if err := godotenv.Load(".env"); err != nil { // Fallback for when running from cmd/importer if err := godotenv.Load("../../.env"); err != nil { log.Println("Warning: .env file not found in .env or ../../.env") } } dbURL := os.Getenv("DB_DSN") if dbURL == "" { log.Fatal("DB_DSN is not set") } db, err := sql.Open("pgx", dbURL) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } defer db.Close() if err := db.Ping(); err != nil { log.Fatalf("Failed to ping database: %v", err) } // Open CSV File filename := "importacao_fots.csv" f, err := os.Open(filename) if err != nil { log.Fatalf("Failed to open CSV file: %v", err) } defer f.Close() // Use Windows-1252 Decoder r := csv.NewReader(transform.NewReader(f, charmap.Windows1252.NewDecoder())) r.Comma = ';' r.LazyQuotes = true // Allow messy quotes // Read Header header, err := r.Read() if err != nil { log.Fatalf("Failed to read header: %v", err) } fmt.Printf("Header: %v\n", header) ctx := context.Background() rowsProcessed := 0 rowsInserted := 0 rowsFailed := 0 for { record, err := r.Read() if err == io.EOF { break } if err != nil { log.Printf("Error reading row %d: %v. Skipping.", rowsProcessed+1, err) rowsFailed++ continue } rowsProcessed++ // Map Columns (Indices based on "FOT;Empresa;EF I / EF II;Observa‡äes;Institui‡Æo;Ano Formatura;Cidade;Estado;Gastos Capta‡Æo;Pr‚ Venda") // 0: FOT // 1: Empresa // 2: Cursos (was EF I / EF II) // 3: Observacoes // 4: Instituicao // 5: Ano Formatura // 6: Cidade // 7: Estado // 8: Gastos Captacao // 9: Pre Venda fotStr := strings.TrimSpace(record[0]) empresaName := strings.TrimSpace(record[1]) cursoName := strings.TrimSpace(record[2]) observacoes := strings.TrimSpace(record[3]) instituicao := strings.TrimSpace(record[4]) anoFormatura := strings.TrimSpace(record[5]) cidade := strings.TrimSpace(record[6]) estado := strings.TrimSpace(record[7]) gastosStr := strings.TrimSpace(record[8]) preVendaStr := strings.TrimSpace(record[9]) // Basic Validation if fotStr == "" || fotStr == "FOT" { // Skip header repetition or empty lines continue } fot, err := strconv.Atoi(fotStr) if err != nil { log.Printf("Row %d: Invalid FOT '%s'. Skipping.", rowsProcessed, fotStr) rowsFailed++ continue } // 1. Upsert Empresa var empresaID string if empresaName == "" { empresaName = "Não Identificada" } err = db.QueryRowContext(ctx, ` INSERT INTO empresas (nome) VALUES ($1) ON CONFLICT (nome) DO UPDATE SET nome = EXCLUDED.nome RETURNING id`, empresaName).Scan(&empresaID) if err != nil { log.Printf("Row %d: Failed to upsert empresa '%s': %v", rowsProcessed, empresaName, err) rowsFailed++ continue } // 2. Upsert Curso var cursoID string if cursoName == "" || cursoName == "-" { cursoName = "Geral" } err = db.QueryRowContext(ctx, ` INSERT INTO cursos (nome) VALUES ($1) ON CONFLICT (nome) DO UPDATE SET nome = EXCLUDED.nome RETURNING id`, cursoName).Scan(&cursoID) if err != nil { log.Printf("Row %d: Failed to upsert curso '%s': %v", rowsProcessed, cursoName, err) rowsFailed++ continue } // 3. Upsert Ano Formatura var anoID string if anoFormatura == "" { anoFormatura = "Indefinido" } err = db.QueryRowContext(ctx, ` INSERT INTO anos_formaturas (ano_semestre) VALUES ($1) ON CONFLICT (ano_semestre) DO UPDATE SET ano_semestre = EXCLUDED.ano_semestre RETURNING id`, anoFormatura).Scan(&anoID) if err != nil { log.Printf("Row %d: Failed to upsert ano '%s': %v", rowsProcessed, anoFormatura, err) rowsFailed++ continue } // 4. Parse Currency (Gastos Captação) // Format: "R$ 2.176,60" -> 2176.60 gastosVal := 0.0 if gastosStr != "" && gastosStr != "-" { // Remove "R$", "." and trim clean := strings.ReplaceAll(gastosStr, "R$", "") clean = strings.ReplaceAll(clean, ".", "") // Remove thousand separator clean = strings.ReplaceAll(clean, ",", ".") // Replace decimal separator clean = strings.TrimSpace(clean) // Handle trailing/leading spaces hidden chars if any // Just parse float val, err := strconv.ParseFloat(clean, 64) if err == nil { gastosVal = val } else { // log.Printf("Row %d: Warning parsing gastos '%s' -> '%s': %v", rowsProcessed, gastosStr, clean, err) } } // 5. Parse Boolean (Pre Venda) preVenda := false if strings.ToLower(preVendaStr) == "sim" { preVenda = true } // 6. Insert Cadastro FOT _, err = db.ExecContext(ctx, ` INSERT INTO cadastro_fot ( fot, empresa_id, curso_id, ano_formatura_id, instituicao, cidade, estado, observacoes, gastos_captacao, pre_venda ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 ) ON CONFLICT (fot) DO UPDATE SET empresa_id = EXCLUDED.empresa_id, curso_id = EXCLUDED.curso_id, ano_formatura_id = EXCLUDED.ano_formatura_id, instituicao = EXCLUDED.instituicao, cidade = EXCLUDED.cidade, estado = EXCLUDED.estado, observacoes = EXCLUDED.observacoes, gastos_captacao = EXCLUDED.gastos_captacao, pre_venda = EXCLUDED.pre_venda, updated_at = NOW() `, fot, empresaID, cursoID, anoID, instituicao, cidade, estado, observacoes, gastosVal, preVenda, ) if err != nil { log.Printf("Row %d: Failed to insert FOT %d: %v", rowsProcessed, fot, err) rowsFailed++ } else { rowsInserted++ if rowsInserted%50 == 0 { fmt.Printf("Progress: %d records upserted...\n", rowsInserted) } } } fmt.Printf("\n--- Import Summary ---\n") fmt.Printf("Total Rows Processed: %d\n", rowsProcessed) fmt.Printf("Successfully Upserted: %d\n", rowsInserted) fmt.Printf("Failed: %d\n", rowsFailed) }