Turnpike/handle_import.go

173 lines
3.8 KiB
Go
Raw Normal View History

package main
import (
"encoding/csv"
"fmt"
"io"
"net/http"
"strings"
)
type ImportResult struct {
Inserted int `json:"inserted"`
Skipped int `json:"skipped"`
Errors []string `json:"errors"`
}
func (app *App) handleImport(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(10 << 20); err != nil {
writeError(w, "invalid form", http.StatusBadRequest)
return
}
file, _, err := r.FormFile("csv")
if err != nil {
writeError(w, "csv file required", http.StatusBadRequest)
return
}
defer file.Close()
result, err := app.importCSV(file)
if err != nil {
writeError(w, err.Error(), http.StatusBadRequest)
return
}
if result.Errors == nil {
result.Errors = []string{}
}
writeJSON(w, result)
}
func (app *App) importCSV(r io.Reader) (ImportResult, error) {
reader := csv.NewReader(r)
reader.TrimLeadingSpace = true
reader.LazyQuotes = true
header, err := reader.Read()
if err != nil {
return ImportResult{}, fmt.Errorf("reading header: %w", err)
}
if len(header) > 0 {
header[0] = strings.TrimPrefix(header[0], "\xef\xbb\xbf") // strip BOM
}
colIndex := make(map[string]int)
for i, h := range header {
colIndex[strings.ToLower(strings.TrimSpace(h))] = i
}
var (
nameIdx, emailIdx, ticketIDIdx, ticketTypeIdx int
hasEmail, hasTicketID, hasTicketType bool
isCrowdWork bool
)
if idx, ok := colIndex["patron name"]; ok {
// CrowdWork / ticketing platform format
isCrowdWork = true
nameIdx = idx
if idx, ok := colIndex["patron email"]; ok {
emailIdx, hasEmail = idx, true
}
if idx, ok := colIndex["tier name"]; ok {
ticketTypeIdx, hasTicketType = idx, true
}
if idx, ok := colIndex["order number"]; ok {
ticketIDIdx, hasTicketID = idx, true
}
} else if idx, ok := colIndex["name"]; ok {
// Generic format
nameIdx = idx
if idx, ok := colIndex["email"]; ok {
emailIdx, hasEmail = idx, true
}
if idx, ok := colIndex["ticket_id"]; ok {
ticketIDIdx, hasTicketID = idx, true
}
if idx, ok := colIndex["ticket_type"]; ok {
ticketTypeIdx, hasTicketType = idx, true
}
} else {
return ImportResult{}, fmt.Errorf("CSV must have a 'name' or 'patron name' column")
}
var result ImportResult
lineNum := 1
for {
record, err := reader.Read()
if err == io.EOF {
break
}
lineNum++
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("line %d: %v", lineNum, err))
continue
}
name := strings.TrimSpace(csvGet(record, nameIdx))
if name == "" {
continue
}
email := ""
if hasEmail {
email = strings.TrimSpace(csvGet(record, emailIdx))
}
externalID := ""
if hasTicketID {
externalID = strings.TrimSpace(csvGet(record, ticketIDIdx))
}
ticketType := ""
if hasTicketType {
ticketType = strings.TrimSpace(csvGet(record, ticketTypeIdx))
}
source := "manual"
orderID := ""
if isCrowdWork {
source = "crowdwork"
orderID = externalID
}
// Find or create participant when email is present.
var participantID *int
if email != "" {
p, _, err := app.upsertParticipant(email, name)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("line %d (%s): participant: %v", lineNum, name, err))
continue
}
if p != nil {
participantID = &p.ID
}
}
_, err = app.createTicket(Ticket{
ParticipantID: participantID,
Name: name,
TicketType: ticketType,
Source: source,
ExternalID: externalID,
OrderID: orderID,
})
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
result.Skipped++
} else {
result.Errors = append(result.Errors, fmt.Sprintf("line %d (%s): %v", lineNum, name, err))
}
continue
}
result.Inserted++
}
return result, nil
}
func csvGet(record []string, idx int) string {
if idx < len(record) {
return record[idx]
}
return ""
}