package handlers import ( "bytes" "io" "net/http" "time" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/roadmap" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // ============================================================================ // Import Workflow // ============================================================================ // UploadImport handles file upload for import // POST /sdk/v1/roadmaps/import/upload func (h *RoadmapHandlers) UploadImport(c *gin.Context) { tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) // Get file from form file, header, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"}) return } defer file.Close() // Read file content buf := bytes.Buffer{} if _, err := io.Copy(&buf, file); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"}) return } // Detect format format := roadmap.ImportFormat("") filename := header.Filename contentType := header.Header.Get("Content-Type") // Create import job job := &roadmap.ImportJob{ TenantID: tenantID, Filename: filename, FileSize: header.Size, ContentType: contentType, Status: "pending", CreatedBy: userID, } if err := h.store.CreateImportJob(c.Request.Context(), job); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Parse the file job.Status = "parsing" h.store.UpdateImportJob(c.Request.Context(), job) result, err := h.parser.ParseFile(buf.Bytes(), filename, contentType) if err != nil { job.Status = "failed" job.ErrorMessage = err.Error() h.store.UpdateImportJob(c.Request.Context(), job) c.JSON(http.StatusBadRequest, gin.H{ "error": "failed to parse file", "detail": err.Error(), }) return } // Update job with parsed data job.Status = "parsed" job.Format = format job.TotalRows = result.TotalRows job.ValidRows = result.ValidRows job.InvalidRows = result.InvalidRows job.ParsedItems = result.Items h.store.UpdateImportJob(c.Request.Context(), job) c.JSON(http.StatusOK, roadmap.ImportParseResponse{ JobID: job.ID, Status: job.Status, TotalRows: result.TotalRows, ValidRows: result.ValidRows, InvalidRows: result.InvalidRows, Items: result.Items, ColumnMap: buildColumnMap(result.Columns), }) } // GetImportJob returns the status of an import job // GET /sdk/v1/roadmaps/import/:jobId func (h *RoadmapHandlers) GetImportJob(c *gin.Context) { jobID, err := uuid.Parse(c.Param("jobId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"}) return } job, err := h.store.GetImportJob(c.Request.Context(), jobID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if job == nil { c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"}) return } c.JSON(http.StatusOK, gin.H{ "job": job, "items": job.ParsedItems, }) } // ConfirmImport confirms and executes the import // POST /sdk/v1/roadmaps/import/:jobId/confirm func (h *RoadmapHandlers) ConfirmImport(c *gin.Context) { jobID, err := uuid.Parse(c.Param("jobId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"}) return } var req roadmap.ImportConfirmRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } job, err := h.store.GetImportJob(c.Request.Context(), jobID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if job == nil { c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"}) return } if job.Status != "parsed" { c.JSON(http.StatusBadRequest, gin.H{ "error": "job is not ready for confirmation", "status": job.Status, }) return } tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) // Create or use existing roadmap var roadmapID uuid.UUID if req.RoadmapID != nil { roadmapID = *req.RoadmapID // Verify roadmap exists r, err := h.store.GetRoadmap(c.Request.Context(), roadmapID) if err != nil || r == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "roadmap not found"}) return } } else { // Create new roadmap title := req.RoadmapTitle if title == "" { title = "Imported Roadmap - " + job.Filename } r := &roadmap.Roadmap{ TenantID: tenantID, Title: title, Status: "active", CreatedBy: userID, } if err := h.store.CreateRoadmap(c.Request.Context(), r); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } roadmapID = r.ID } // Determine which rows to import selectedRows := make(map[int]bool) if len(req.SelectedRows) > 0 { for _, row := range req.SelectedRows { selectedRows[row] = true } } // Convert parsed items to roadmap items var items []roadmap.RoadmapItem var importedCount, skippedCount int for i, parsed := range job.ParsedItems { // Skip invalid items if !parsed.IsValid { skippedCount++ continue } // Skip unselected rows if selection was specified if len(selectedRows) > 0 && !selectedRows[parsed.RowNumber] { skippedCount++ continue } item := roadmap.RoadmapItem{ RoadmapID: roadmapID, Title: parsed.Data.Title, Description: parsed.Data.Description, Category: parsed.Data.Category, Priority: parsed.Data.Priority, Status: parsed.Data.Status, ControlID: parsed.Data.ControlID, RegulationRef: parsed.Data.RegulationRef, GapID: parsed.Data.GapID, EffortDays: parsed.Data.EffortDays, AssigneeName: parsed.Data.AssigneeName, Department: parsed.Data.Department, PlannedStart: parsed.Data.PlannedStart, PlannedEnd: parsed.Data.PlannedEnd, Notes: parsed.Data.Notes, SourceRow: parsed.RowNumber, SourceFile: job.Filename, SortOrder: i, } // Apply auto-mappings if requested if req.ApplyMappings { if parsed.MatchedControl != "" { item.ControlID = parsed.MatchedControl } if parsed.MatchedRegulation != "" { item.RegulationRef = parsed.MatchedRegulation } if parsed.MatchedGap != "" { item.GapID = parsed.MatchedGap } } // Set defaults if item.Status == "" { item.Status = roadmap.ItemStatusPlanned } if item.Priority == "" { item.Priority = roadmap.ItemPriorityMedium } if item.Category == "" { item.Category = roadmap.ItemCategoryTechnical } items = append(items, item) importedCount++ } // Bulk create items if len(items) > 0 { if err := h.store.BulkCreateItems(c.Request.Context(), items); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } // Update roadmap progress h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID) // Update job status now := time.Now().UTC() job.Status = "completed" job.RoadmapID = &roadmapID job.ImportedItems = importedCount job.CompletedAt = &now h.store.UpdateImportJob(c.Request.Context(), job) c.JSON(http.StatusOK, roadmap.ImportConfirmResponse{ RoadmapID: roadmapID, ImportedItems: importedCount, SkippedItems: skippedCount, Message: "Import completed successfully", }) } // ============================================================================ // Helper Functions // ============================================================================ func buildColumnMap(columns []roadmap.DetectedColumn) map[string]string { result := make(map[string]string) for _, col := range columns { if col.MappedTo != "" { result[col.Header] = col.MappedTo } } return result }