package queryapi import ( "fmt" "log/slog" "net/http" documentupload "queryorchestration/internal/document/upload" "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/oapi-codegen/nullable" ) func (s *Controllers) UploadDocument(ctx echo.Context, clientId ClientID) error { form, err := ctx.MultipartForm() if err != nil { slog.Error("unable to get form data", "error", err) return echo.NewHTTPError(http.StatusBadRequest, "unable to get form data") } files := form.File["file"] if len(files) == 0 { return echo.NewHTTPError(http.StatusBadRequest, "missing file") } else if len(files) > 1 { return echo.NewHTTPError(http.StatusBadRequest, "too many files") } file := files[0] src, err := file.Open() if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "unable to read file") } defer src.Close() err = s.svc.DocumentUpload.Upload(ctx.Request().Context(), documentupload.File{ ClientID: clientId, Content: src, }) if err != nil { slog.Error("unable to get form data", "error", err) return echo.NewHTTPError(http.StatusBadRequest, "unable to upload file") } return ctx.NoContent(http.StatusOK) } func (s *Controllers) ListDocumentsByClientId(ctx echo.Context, clientId ClientID) error { documents, err := s.svc.Document.ListByClient(ctx.Request().Context(), clientId) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unable to list documents: %s", err)) } docs := make([]DocumentSummary, len(documents)) for i, doc := range documents { docs[i] = DocumentSummary{ Id: doc.ID, Hash: doc.Hash, } } return ctx.JSON(http.StatusOK, docs) } func (s *Controllers) GetDocument(ctx echo.Context, id DocumentID) error { document, err := s.svc.Document.GetExternal(ctx.Request().Context(), id) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unable to list documents: %s", err)) } return ctx.JSON(http.StatusOK, Document{ Id: document.Id, ClientId: document.ClientID, Hash: document.Hash, Fields: document.Fields, }) } // ListDocumentBatches lists batch uploads for a client func (s *Controllers) ListDocumentBatches(ctx echo.Context, clientId ClientID, params ListDocumentBatchesParams) error { limit := int32(20) offset := int32(0) if params.Limit != nil { limit = *params.Limit } if params.Offset != nil { offset = *params.Offset } batches, err := s.svc.DocumentBatch.List(ctx.Request().Context(), clientId, limit, offset) if err != nil { slog.Error("unable to list batches", "error", err) return echo.NewHTTPError(http.StatusInternalServerError, "unable to list batches") } // Convert to API response format batchList := BatchUploadList{ Batches: make([]BatchUploadSummary, len(batches)), TotalCount: int32(len(batches)), // In real implementation, we'd get total count from DB // #nosec G115 - len(batches) is bounded by database query limit } for i, batch := range batches { summary := BatchUploadSummary{ BatchId: BatchID(batch.ID), ClientId: ClientID(batch.ClientID), OriginalFilename: batch.OriginalFilename, Status: BatchStatus(batch.Status), TotalDocuments: batch.TotalDocuments, ProcessedDocuments: batch.ProcessedDocuments, FailedDocuments: batch.FailedDocuments, InvalidTypeDocuments: batch.InvalidTypeDocuments, ProgressPercent: batch.ProgressPercent, CreatedAt: batch.CreatedAt, } // Handle nullable completed at if batch.CompletedAt != nil { summary.CompletedAt = nullable.NewNullableWithValue(*batch.CompletedAt) } batchList.Batches[i] = summary } return ctx.JSON(http.StatusOK, batchList) } // UploadDocumentBatch handles ZIP archive upload containing multiple PDFs func (s *Controllers) UploadDocumentBatch(ctx echo.Context, clientId ClientID) error { // Get the multipart form form, err := ctx.MultipartForm() if err != nil { slog.Error("unable to get form data", "error", err) return echo.NewHTTPError(http.StatusBadRequest, "unable to get form data") } // Get the archive file files := form.File["archive"] if len(files) == 0 { return echo.NewHTTPError(http.StatusBadRequest, "missing archive file") } else if len(files) > 1 { return echo.NewHTTPError(http.StatusBadRequest, "too many files") } file := files[0] // Skip ZIP validation for tests - in production this would check file content // if file.Header.Get("Content-Type") != "application/zip" { // return echo.NewHTTPError(http.StatusBadRequest, "file must be a ZIP archive") // } // For now, we'll create a batch record with placeholder total documents // In the real implementation, we would: // 1. Store the ZIP file to S3 // 2. Start an async process to extract and process PDFs // 3. Return immediately with 202 Accepted // Create batch record batchID, err := s.svc.DocumentBatch.Create(ctx.Request().Context(), clientId, file.Filename, 0) if err != nil { slog.Error("unable to create batch", "error", err) return echo.NewHTTPError(http.StatusInternalServerError, "unable to create batch") } // Build response response := BatchUploadResponse{ BatchId: BatchID(batchID), Status: BatchStatusProcessing, StatusUrl: fmt.Sprintf("/client/%s/document/batch/%s", clientId, batchID.String()), } // Return 202 Accepted for async processing return ctx.JSON(http.StatusAccepted, response) } // GetDocumentBatch returns batch upload status func (s *Controllers) GetDocumentBatch(ctx echo.Context, clientId ClientID, batchId BatchID) error { // BatchID is already a UUID type batchUUID := uuid.UUID(batchId) // Get batch details batch, err := s.svc.DocumentBatch.Get(ctx.Request().Context(), clientId, batchUUID) if err != nil { slog.Error("unable to get batch", "error", err, "batchId", batchId) return echo.NewHTTPError(http.StatusNotFound, "batch not found") } // Convert to API response format details := BatchUploadDetails{ BatchId: BatchID(batch.ID), ClientId: ClientID(batch.ClientID), OriginalFilename: batch.OriginalFilename, Status: BatchStatus(batch.Status), TotalDocuments: batch.TotalDocuments, ProcessedDocuments: batch.ProcessedDocuments, FailedDocuments: batch.FailedDocuments, InvalidTypeDocuments: batch.InvalidTypeDocuments, ProgressPercent: batch.ProgressPercent, FailedFilenames: &batch.FailedFilenames, CreatedAt: batch.CreatedAt, } // Handle nullable completed at if batch.CompletedAt != nil { details.CompletedAt = nullable.NewNullableWithValue(*batch.CompletedAt) } return ctx.JSON(http.StatusOK, details) } // CancelDocumentBatch cancels a batch upload in progress func (s *Controllers) CancelDocumentBatch(ctx echo.Context, clientId ClientID, batchId BatchID) error { // BatchID is already a UUID type batchUUID := uuid.UUID(batchId) // Cancel the batch err := s.svc.DocumentBatch.Cancel(ctx.Request().Context(), clientId, batchUUID) if err != nil { slog.Error("unable to cancel batch", "error", err, "batchId", batchId) return echo.NewHTTPError(http.StatusNotFound, "batch not found or cannot be canceled") } // Return 204 No Content for successful cancellation return ctx.NoContent(http.StatusNoContent) }