package handler import ( "encoding/json" "net/http" "financeiro-carvalho/internal/model" "financeiro-carvalho/internal/service" ) const maxUploadSize = 10 << 20 // 10 MB type ImportHandler struct { svc *service.ImportService gameSvc *service.GameService pendingSvc *service.PendingBillService } func NewImportHandler(svc *service.ImportService, gameSvc *service.GameService, pendingSvc *service.PendingBillService) *ImportHandler { return &ImportHandler{svc: svc, gameSvc: gameSvc, pendingSvc: pendingSvc} } // Preview parses the uploaded file and returns rows with duplicate flags. // POST /api/imports/preview multipart: file + optional csv_mapping JSON field func (h *ImportHandler) Preview(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) if err := r.ParseMultipartForm(maxUploadSize); err != nil { respondError(w, http.StatusBadRequest, "file too large or invalid form") return } file, header, err := r.FormFile("file") if err != nil { respondError(w, http.StatusBadRequest, "field 'file' is required") return } defer file.Close() var csvMapping *model.CSVMapping if raw := r.FormValue("csv_mapping"); raw != "" { var m model.CSVMapping if err := json.Unmarshal([]byte(raw), &m); err != nil { respondError(w, http.StatusBadRequest, "invalid csv_mapping JSON") return } csvMapping = &m } rows, parseErrs, err := h.svc.Preview(r.Context(), header.Filename, file, csvMapping) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } respondJSON(w, http.StatusOK, map[string]any{ "rows": rows, "parse_errors": parseErrs, }) } // Confirm saves the rows provided in the request body (after user review). // POST /api/imports/confirm // When is_credit_card=true and payment_date is in the future, saves as pending instead of transactions. func (h *ImportHandler) Confirm(w http.ResponseWriter, r *http.Request) { var body struct { Filename string `json:"filename"` Rows []model.ImportRow `json:"rows"` ParseErrCount int `json:"parse_error_count"` IsCreditCard bool `json:"is_credit_card"` PaymentDate string `json:"payment_date"` // YYYY-MM-DD } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { respondError(w, http.StatusBadRequest, "invalid JSON") return } if body.Filename == "" { respondError(w, http.StatusBadRequest, "filename is required") return } if body.IsCreditCard && body.PaymentDate != "" { saved, pending, err := h.pendingSvc.MaybeSaveAsPending(r.Context(), body.Filename, body.PaymentDate, body.Rows) if err != nil { respondError(w, http.StatusInternalServerError, "failed to save pending bill") return } if saved { respondJSON(w, http.StatusOK, map[string]any{ "pending": true, "pending_bill": pending, "imported": 0, "duplicates": len(body.Rows) - len(pending.Rows), "errors": body.ParseErrCount, }) return } } result, err := h.svc.Confirm(r.Context(), body.Filename, body.Rows, body.ParseErrCount) if err != nil { respondError(w, http.StatusInternalServerError, "failed to save transactions") return } h.gameSvc.NotifyAction(r.Context(), "import_confirmed", map[string]any{"count": result.Imported}) respondJSON(w, http.StatusOK, result) }