# Contract: Import / Export Workflows Version: 1.0 ## Import Workflow Recommended stages: 1. `Upload` 2. `Preview / Validate` 3. `Confirm` 4. `Execute` 5. `Result summary` Rules: - Validation preview must be human-readable (table/list), not raw JSON only. - Warnings and errors should be shown per row and in aggregate summary. - Confirm step should clearly communicate scope and side effects. ## Export Workflow - User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists. - Export format should be explicit (`csv`, `json`, etc.). - Download response must set: - `Content-Type: text/csv; charset=utf-8` - `Content-Disposition: attachment; filename="..."` ## CSV Format Rules (Excel-compatible) These rules are **mandatory** whenever CSV is exported for spreadsheet users. ### Encoding and BOM - Write UTF-8 BOM (`\xEF\xBB\xBF`) as the very first bytes of the response. - Without BOM, Excel on Windows opens UTF-8 CSV as ANSI and garbles Cyrillic/special characters. ```go w.Write([]byte{0xEF, 0xBB, 0xBF}) ``` ### Delimiter - Use **semicolon** (`;`) as the field delimiter, not comma. - Excel in Russian/European locale uses semicolon as the list separator. - Comma-delimited files open as a single column in these locales. ### Numbers - Write decimal numbers with a **comma** as the decimal separator: `1 234,56` — not `1234.56`. - Excel in Russian locale does not recognize period as a decimal separator in numeric cells. - Format integers and floats explicitly; do not rely on Go's default `%v` or `strconv.FormatFloat`. - Use a thin non-breaking space (`\u202F`) or regular space as a thousands separator when the value benefits from readability (e.g. prices, quantities > 9999). ```go // correct fmt.Sprintf("%.2f", price) // then replace "." -> "," strings.ReplaceAll(fmt.Sprintf("%.2f", price), ".", ",") // wrong — produces "1234.56", Excel treats it as text in RU locale fmt.Sprintf("%.2f", price) ``` ### Dates - Write dates as `DD.MM.YYYY` — the format Excel in Russian locale parses as a date cell automatically. - Do not use ISO 8601 (`2006-01-02`) for user-facing CSV; it is not auto-recognized as a date in RU locale. ### Text quoting - Wrap any field that contains the delimiter (`;`), a newline, or a double-quote in double quotes. - Escape embedded double-quotes by doubling them: `""`. - Use `encoding/csv` with `csv.Writer` and set `csv.Writer.Comma = ';'`; it handles quoting automatically. ## Streaming Export Architecture (Go) For exports with potentially large row counts use a 3-layer streaming pattern. Never load all rows into memory before writing — stream directly to the response writer. ``` Handler → sets HTTP headers + writes BOM → calls Service Service → delegates to Repository with a row callback Repository → queries in batches → calls callback per row Handler/Service → csv.Writer.Flush() after all rows ``` ```go // Handler func ExportCSV(c *gin.Context) { c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", `attachment; filename="export.csv"`) c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) // BOM w := csv.NewWriter(c.Writer) w.Comma = ';' w.Write([]string{"ID", "Name", "Price"}) // header row err := svc.StreamRows(ctx, filters, func(row Row) error { return w.Write([]string{row.ID, row.Name, formatPrice(row.Price)}) }) w.Flush() if err != nil { // headers already sent — log only, cannot change status slog.Error("csv export failed mid-stream", "err", err) } } // Repository — batch fetch with callback func (r *Repo) StreamRows(ctx, filters, fn func(Row) error) error { rows, err := r.db.QueryContext(ctx, query, args...) // ... scan and call fn(row) for each row } ``` - Use `JOIN` in the repository query to avoid N+1 per row. - Batch size is optional; streaming row-by-row is fine for most datasets. - Always call `w.Flush()` after the loop — `csv.Writer` buffers internally. ## Error Handling - Import errors should map to clear user-facing messages. - Export errors after streaming starts must be logged server-side only — HTTP headers are already sent and the status code cannot be changed mid-stream.