fix broken upload reported csv with column selection

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-12-16 20:11:36 +01:00
parent 72a5f26562
commit 1881331ac0
4 changed files with 178 additions and 40 deletions

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/csv"
"io"
"strconv"
"strings"
"time"
@@ -1104,8 +1105,35 @@ func (c *Campaign) UploadReportedCSV(g *gin.Context) {
c.Logger.Debugw("processing CSV", "rows", len(records), "headers", records[0])
// get required column indices from query params
emailColStr := g.Query("emailColumn")
if emailColStr == "" {
c.Response.ValidationFailed(g, "emailColumn", errors.New("emailColumn query parameter is required"))
return
}
dateColStr := g.Query("dateColumn")
if dateColStr == "" {
c.Response.ValidationFailed(g, "dateColumn", errors.New("dateColumn query parameter is required"))
return
}
emailColIdx, err := strconv.Atoi(emailColStr)
if err != nil || emailColIdx < 0 {
c.Response.ValidationFailed(g, "emailColumn", errors.New("emailColumn must be a valid non-negative integer"))
return
}
dateColIdx, err := strconv.Atoi(dateColStr)
if err != nil || dateColIdx < 0 {
c.Response.ValidationFailed(g, "dateColumn", errors.New("dateColumn must be a valid non-negative integer"))
return
}
c.Logger.Debugw("using provided column indices", "emailColumn", emailColIdx, "dateColumn", dateColIdx)
// process CSV
processed, skipped, err := c.CampaignService.ProcessReportedCSV(g.Request.Context(), session, id, records)
processed, skipped, err := c.CampaignService.ProcessReportedCSV(g.Request.Context(), session, id, records, emailColIdx, dateColIdx)
if err != nil {
c.Logger.Errorw("failed to process reported CSV", "error", err)
if ok := c.handleErrors(g, err); !ok {

View File

@@ -4186,6 +4186,8 @@ func (c *Campaign) ProcessReportedCSV(
session *model.Session,
campaignID *uuid.UUID,
records [][]string,
emailColumnIndex int,
dateColumnIndex int,
) (int, int, error) {
ae := NewAuditEvent("Campaign.ProcessReportedCSV", session)
ae.Details["campaignID"] = campaignID.String()
@@ -4208,33 +4210,25 @@ func (c *Campaign) ProcessReportedCSV(
return 0, 0, errs.Wrap(err)
}
// validate CSV headers
headers := records[0]
reportedByIndex := -1
dateReportedIndex := -1
c.Logger.Debugw("processing CSV headers", "headers", headers)
for i, header := range headers {
switch strings.ToLower(strings.TrimSpace(header)) {
case "reported by":
reportedByIndex = i
c.Logger.Debugw("found reported by column", "index", i)
case "date reporter (utc+02:00)", "date reported(utc+02:00)", "date reported", "date reporter":
dateReportedIndex = i
c.Logger.Debugw("found date column", "index", i, "header", header)
}
if len(records) < 2 {
return 0, 0, errs.NewValidationError(errors.New("CSV must have at least a header row and one data row"))
}
if reportedByIndex == -1 {
c.Logger.Errorw("CSV missing required column", "expected", "reported by", "headers", headers)
return 0, 0, errs.NewValidationError(errors.New("CSV must have 'reported by' column"))
// validate column indices
if emailColumnIndex < 0 || emailColumnIndex >= len(records[0]) {
c.Logger.Errorw("invalid email column index", "index", emailColumnIndex, "columnCount", len(records[0]))
return 0, 0, errs.NewValidationError(errors.New("invalid email column index"))
}
if dateReportedIndex == -1 {
c.Logger.Errorw("CSV missing required date column", "expected", "date reported(utc+02:00)", "headers", headers)
return 0, 0, errs.NewValidationError(errors.New("CSV must have 'date reporter (utc+02:00)' or similar date column"))
if dateColumnIndex < 0 || dateColumnIndex >= len(records[0]) {
c.Logger.Errorw("invalid date column index", "index", dateColumnIndex, "columnCount", len(records[0]))
return 0, 0, errs.NewValidationError(errors.New("invalid date column index"))
}
reportedByIndex := emailColumnIndex
dateReportedIndex := dateColumnIndex
c.Logger.Infow("processing CSV columns", "emailColumn", reportedByIndex, "dateColumn", dateReportedIndex)
processed := 0
skipped := 0
reportedEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_REPORTED]
@@ -4258,30 +4252,31 @@ func (c *Campaign) ProcessReportedCSV(
// parse date - try multiple formats and handle timezone
var parsedDate time.Time
dateFormats := []string{
"2006-01-02T15:04:05",
"2006-01-02 15:04:05",
// iso 8601 with fractional seconds (microsoft format)
"2006-01-02T15:04:05.999999999Z",
"2006-01-02T15:04:05.999999999-07:00",
"2006-01-02T15:04:05.999999999+02:00",
"2006-01-02T15:04:05.999999999+01:00",
// iso 8601 without fractional seconds
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05-07:00",
"2006-01-02T15:04:05+02:00",
"2006-01-02T15:04:05+01:00",
"2006-01-02T15:04:05",
// other common formats
"2006-01-02 15:04:05",
"2006-01-02",
"01/02/2006 15:04:05",
"01/02/2006",
"02-01-2006 15:04:05",
"02-01-2006",
"2006/01/02 15:04:05",
"2006/01/02",
}
dateParseError := true
for _, format := range dateFormats {
if pd, err := time.Parse(format, dateReported); err == nil {
// if the parsed date has no timezone info and the header mentions UTC+02:00,
// assume the time is in UTC+02:00 and convert to UTC
if pd.Location() == time.UTC && strings.Contains(strings.ToLower(headers[dateReportedIndex]), "utc+02:00") {
// treat as UTC+02:00 and convert to UTC
loc, _ := time.LoadLocation("Europe/Berlin") // UTC+2 (or use FixedZone)
if loc != nil {
pd = time.Date(pd.Year(), pd.Month(), pd.Day(), pd.Hour(), pd.Minute(), pd.Second(), pd.Nanosecond(), loc).UTC()
}
}
parsedDate = pd
dateParseError = false
break