package handlers import ( "context" "fmt" "net/http" "strings" "github.com/breakpilot/consent-service/internal/middleware" "github.com/breakpilot/consent-service/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // ======================================== // ADMIN ENDPOINTS - Document Management // ======================================== // AdminGetDocuments returns all documents (including inactive) for admin func (h *Handler) AdminGetDocuments(c *gin.Context) { ctx := context.Background() rows, err := h.db.Pool.Query(ctx, ` SELECT id, type, name, description, is_mandatory, is_active, sort_order, created_at, updated_at FROM legal_documents ORDER BY sort_order ASC, created_at DESC `) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch documents"}) return } defer rows.Close() var documents []models.LegalDocument for rows.Next() { var doc models.LegalDocument if err := rows.Scan(&doc.ID, &doc.Type, &doc.Name, &doc.Description, &doc.IsMandatory, &doc.IsActive, &doc.SortOrder, &doc.CreatedAt, &doc.UpdatedAt); err != nil { continue } documents = append(documents, doc) } c.JSON(http.StatusOK, gin.H{"documents": documents}) } // AdminCreateDocument creates a new legal document func (h *Handler) AdminCreateDocument(c *gin.Context) { var req models.CreateDocumentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } ctx := context.Background() var docID uuid.UUID err := h.db.Pool.QueryRow(ctx, ` INSERT INTO legal_documents (type, name, description, is_mandatory) VALUES ($1, $2, $3, $4) RETURNING id `, req.Type, req.Name, req.Description, req.IsMandatory).Scan(&docID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create document"}) return } c.JSON(http.StatusCreated, gin.H{ "message": "Document created successfully", "id": docID, }) } // AdminUpdateDocument updates a legal document func (h *Handler) AdminUpdateDocument(c *gin.Context) { docID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"}) return } var req struct { Name *string `json:"name"` Description *string `json:"description"` IsMandatory *bool `json:"is_mandatory"` IsActive *bool `json:"is_active"` SortOrder *int `json:"sort_order"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } ctx := context.Background() result, err := h.db.Pool.Exec(ctx, ` UPDATE legal_documents SET name = COALESCE($2, name), description = COALESCE($3, description), is_mandatory = COALESCE($4, is_mandatory), is_active = COALESCE($5, is_active), sort_order = COALESCE($6, sort_order), updated_at = NOW() WHERE id = $1 `, docID, req.Name, req.Description, req.IsMandatory, req.IsActive, req.SortOrder) if err != nil || result.RowsAffected() == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "Document updated successfully"}) } // AdminDeleteDocument soft-deletes a document (sets is_active to false) func (h *Handler) AdminDeleteDocument(c *gin.Context) { docID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"}) return } ctx := context.Background() result, err := h.db.Pool.Exec(ctx, ` UPDATE legal_documents SET is_active = false, updated_at = NOW() WHERE id = $1 `, docID) if err != nil || result.RowsAffected() == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "Document deleted successfully"}) } // ======================================== // ADMIN ENDPOINTS - Version Management // ======================================== // AdminGetVersions returns all versions for a document func (h *Handler) AdminGetVersions(c *gin.Context) { docID, err := uuid.Parse(c.Param("docId")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"}) return } ctx := context.Background() rows, err := h.db.Pool.Query(ctx, ` SELECT id, document_id, version, language, title, content, summary, status, published_at, scheduled_publish_at, created_by, approved_by, approved_at, created_at, updated_at FROM document_versions WHERE document_id = $1 ORDER BY created_at DESC `, docID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch versions"}) return } defer rows.Close() var versions []models.DocumentVersion for rows.Next() { var v models.DocumentVersion if err := rows.Scan(&v.ID, &v.DocumentID, &v.Version, &v.Language, &v.Title, &v.Content, &v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy, &v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt); err != nil { continue } versions = append(versions, v) } c.JSON(http.StatusOK, gin.H{"versions": versions}) } // AdminCreateVersion creates a new document version func (h *Handler) AdminCreateVersion(c *gin.Context) { var req models.CreateVersionRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } docID, err := uuid.Parse(req.DocumentID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"}) return } userID, _ := middleware.GetUserID(c) ctx := context.Background() var versionID uuid.UUID err = h.db.Pool.QueryRow(ctx, ` INSERT INTO document_versions (document_id, version, language, title, content, summary, status, created_by) VALUES ($1, $2, $3, $4, $5, $6, 'draft', $7) RETURNING id `, docID, req.Version, req.Language, req.Title, req.Content, req.Summary, userID).Scan(&versionID) if err != nil { // Check for unique constraint violation errStr := err.Error() if strings.Contains(errStr, "duplicate key") || strings.Contains(errStr, "unique constraint") { c.JSON(http.StatusConflict, gin.H{"error": "Eine Version mit dieser Versionsnummer und Sprache existiert bereits für dieses Dokument"}) return } // Log the actual error for debugging fmt.Printf("POST /api/v1/admin/versions ✗ %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create version: " + errStr}) return } c.JSON(http.StatusCreated, gin.H{ "message": "Version created successfully", "id": versionID, }) } // AdminUpdateVersion updates a document version func (h *Handler) AdminUpdateVersion(c *gin.Context) { versionID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"}) return } var req models.UpdateVersionRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } ctx := context.Background() // Check if version is in draft or review status (only these can be edited) var status string err = h.db.Pool.QueryRow(ctx, `SELECT status FROM document_versions WHERE id = $1`, versionID).Scan(&status) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"}) return } if status != "draft" && status != "review" { c.JSON(http.StatusBadRequest, gin.H{"error": "Only draft or review versions can be edited"}) return } result, err := h.db.Pool.Exec(ctx, ` UPDATE document_versions SET title = COALESCE($2, title), content = COALESCE($3, content), summary = COALESCE($4, summary), status = COALESCE($5, status), updated_at = NOW() WHERE id = $1 `, versionID, req.Title, req.Content, req.Summary, req.Status) if err != nil || result.RowsAffected() == 0 { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update version"}) return } c.JSON(http.StatusOK, gin.H{"message": "Version updated successfully"}) } // AdminPublishVersion publishes a document version func (h *Handler) AdminPublishVersion(c *gin.Context) { versionID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"}) return } userID, _ := middleware.GetUserID(c) ctx := context.Background() // Check current status var status string err = h.db.Pool.QueryRow(ctx, `SELECT status FROM document_versions WHERE id = $1`, versionID).Scan(&status) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"}) return } if status != "approved" && status != "review" { c.JSON(http.StatusBadRequest, gin.H{"error": "Only approved or review versions can be published"}) return } result, err := h.db.Pool.Exec(ctx, ` UPDATE document_versions SET status = 'published', published_at = NOW(), approved_by = $2, updated_at = NOW() WHERE id = $1 `, versionID, userID) if err != nil || result.RowsAffected() == 0 { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish version"}) return } c.JSON(http.StatusOK, gin.H{"message": "Version published successfully"}) } // AdminArchiveVersion archives a document version func (h *Handler) AdminArchiveVersion(c *gin.Context) { versionID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"}) return } ctx := context.Background() result, err := h.db.Pool.Exec(ctx, ` UPDATE document_versions SET status = 'archived', updated_at = NOW() WHERE id = $1 `, versionID) if err != nil || result.RowsAffected() == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "Version archived successfully"}) } // AdminDeleteVersion permanently deletes a draft/rejected version // Only draft and rejected versions can be deleted. Published versions must be archived. func (h *Handler) AdminDeleteVersion(c *gin.Context) { versionID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"}) return } ctx := context.Background() // First check the version status - only draft/rejected can be deleted var status string var version string var docID uuid.UUID err = h.db.Pool.QueryRow(ctx, ` SELECT status, version, document_id FROM document_versions WHERE id = $1 `, versionID).Scan(&status, &version, &docID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"}) return } // Only allow deletion of draft and rejected versions if status != "draft" && status != "rejected" { c.JSON(http.StatusForbidden, gin.H{ "error": "Cannot delete version", "message": "Only draft or rejected versions can be deleted. Published versions must be archived instead.", "status": status, }) return } // Delete the version result, err := h.db.Pool.Exec(ctx, ` DELETE FROM document_versions WHERE id = $1 `, versionID) if err != nil || result.RowsAffected() == 0 { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete version"}) return } // Log the deletion userID, _ := c.Get("user_id") h.db.Pool.Exec(ctx, ` INSERT INTO consent_audit_log (action, entity_type, entity_id, user_id, details, ip_address, user_agent) VALUES ('version_deleted', 'document_version', $1, $2, $3, $4, $5) `, versionID, userID, "Version "+version+" permanently deleted", c.ClientIP(), c.Request.UserAgent()) c.JSON(http.StatusOK, gin.H{ "message": "Version deleted successfully", "deleted_version": version, "version_id": versionID, }) }