package codes import ( "encoding/csv" "io" "strconv" "time" ) // CSVRow represents one row in the export CSV. // The Code field is the only time the plaintext appears outside the // generation call; it must be streamed directly to the response writer // or written to an encrypted file and never logged. type CSVRow struct { Index int Code string // plaintext activation code (canonical form) Plan PlanCode DurationDays int BatchID int64 Channel BatchChannel GeneratedAt time.Time } // ExportCSV writes the given code rows to w in CSV format. // Columns: index, code, plan, duration_days, batch_id, channel, generated_at_utc // // The caller must ensure that w is the final output destination (HTTP response, // encrypted file, etc.) and that no intermediate buffer retains the data. func ExportCSV(w io.Writer, rows []CSVRow) error { cw := csv.NewWriter(w) // Header row. if err := cw.Write([]string{ "index", "code", "plan", "duration_days", "batch_id", "channel", "generated_at_utc", }); err != nil { return err } for _, r := range rows { rec := []string{ strconv.Itoa(r.Index), r.Code, string(r.Plan), strconv.Itoa(r.DurationDays), strconv.FormatInt(r.BatchID, 10), string(r.Channel), r.GeneratedAt.UTC().Format(time.RFC3339), } if err := cw.Write(rec); err != nil { return err } } cw.Flush() return cw.Error() } // BatchResultToCSVRows converts a BatchResult to a slice of CSVRow for export. // generatedAt should be the time.Now() captured immediately after generation. func BatchResultToCSVRows(br *BatchResult, generatedAt time.Time) []CSVRow { rows := make([]CSVRow, len(br.Codes)) for i, code := range br.Codes { rows[i] = CSVRow{ Index: i + 1, Code: code, Plan: br.PlanCode, DurationDays: br.DurationDays, BatchID: br.BatchID, Channel: br.Channel, GeneratedAt: generatedAt, } } return rows }