package handlers import ( "encoding/json" "net/http" "time" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/vendor" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // VendorHandlers handles vendor-compliance HTTP requests type VendorHandlers struct { store *vendor.Store } // NewVendorHandlers creates new vendor handlers func NewVendorHandlers(store *vendor.Store) *VendorHandlers { return &VendorHandlers{store: store} } // ============================================================================ // Vendor CRUD // ============================================================================ // CreateVendor creates a new vendor // POST /sdk/v1/vendors func (h *VendorHandlers) CreateVendor(c *gin.Context) { var req vendor.CreateVendorRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) v := &vendor.Vendor{ TenantID: tenantID, Name: req.Name, LegalForm: req.LegalForm, Country: req.Country, Address: req.Address, Website: req.Website, ContactName: req.ContactName, ContactEmail: req.ContactEmail, ContactPhone: req.ContactPhone, ContactDepartment: req.ContactDepartment, Role: req.Role, ServiceCategory: req.ServiceCategory, ServiceDescription: req.ServiceDescription, DataAccessLevel: req.DataAccessLevel, ProcessingLocations: req.ProcessingLocations, Certifications: req.Certifications, ReviewFrequency: req.ReviewFrequency, ProcessingActivityIDs: req.ProcessingActivityIDs, TemplateID: req.TemplateID, Status: vendor.VendorStatusActive, CreatedBy: userID.String(), } if err := h.store.CreateVendor(c.Request.Context(), v); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"vendor": v}) } // ListVendors lists all vendors for a tenant // GET /sdk/v1/vendors func (h *VendorHandlers) ListVendors(c *gin.Context) { tenantID := rbac.GetTenantID(c) vendors, err := h.store.ListVendors(c.Request.Context(), tenantID.String()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "vendors": vendors, "total": len(vendors), }) } // GetVendor retrieves a vendor by ID with contracts and findings // GET /sdk/v1/vendors/:id func (h *VendorHandlers) GetVendor(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") v, err := h.store.GetVendor(c.Request.Context(), tenantID.String(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if v == nil { c.JSON(http.StatusNotFound, gin.H{"error": "vendor not found"}) return } contracts, _ := h.store.ListContracts(c.Request.Context(), tenantID.String(), &id) findings, _ := h.store.ListFindings(c.Request.Context(), tenantID.String(), &id, nil) c.JSON(http.StatusOK, gin.H{ "vendor": v, "contracts": contracts, "findings": findings, }) } // UpdateVendor updates a vendor // PUT /sdk/v1/vendors/:id func (h *VendorHandlers) UpdateVendor(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") v, err := h.store.GetVendor(c.Request.Context(), tenantID.String(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if v == nil { c.JSON(http.StatusNotFound, gin.H{"error": "vendor not found"}) return } var req vendor.UpdateVendorRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Apply non-nil fields if req.Name != nil { v.Name = *req.Name } if req.LegalForm != nil { v.LegalForm = *req.LegalForm } if req.Country != nil { v.Country = *req.Country } if req.Address != nil { v.Address = req.Address } if req.Website != nil { v.Website = *req.Website } if req.ContactName != nil { v.ContactName = *req.ContactName } if req.ContactEmail != nil { v.ContactEmail = *req.ContactEmail } if req.ContactPhone != nil { v.ContactPhone = *req.ContactPhone } if req.ContactDepartment != nil { v.ContactDepartment = *req.ContactDepartment } if req.Role != nil { v.Role = *req.Role } if req.ServiceCategory != nil { v.ServiceCategory = *req.ServiceCategory } if req.ServiceDescription != nil { v.ServiceDescription = *req.ServiceDescription } if req.DataAccessLevel != nil { v.DataAccessLevel = *req.DataAccessLevel } if req.ProcessingLocations != nil { v.ProcessingLocations = req.ProcessingLocations } if req.Certifications != nil { v.Certifications = req.Certifications } if req.InherentRiskScore != nil { v.InherentRiskScore = req.InherentRiskScore } if req.ResidualRiskScore != nil { v.ResidualRiskScore = req.ResidualRiskScore } if req.ManualRiskAdjustment != nil { v.ManualRiskAdjustment = req.ManualRiskAdjustment } if req.ReviewFrequency != nil { v.ReviewFrequency = *req.ReviewFrequency } if req.LastReviewDate != nil { v.LastReviewDate = req.LastReviewDate } if req.NextReviewDate != nil { v.NextReviewDate = req.NextReviewDate } if req.ProcessingActivityIDs != nil { v.ProcessingActivityIDs = req.ProcessingActivityIDs } if req.Status != nil { v.Status = *req.Status } if req.TemplateID != nil { v.TemplateID = req.TemplateID } if err := h.store.UpdateVendor(c.Request.Context(), v); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"vendor": v}) } // DeleteVendor deletes a vendor // DELETE /sdk/v1/vendors/:id func (h *VendorHandlers) DeleteVendor(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") if err := h.store.DeleteVendor(c.Request.Context(), tenantID.String(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "vendor deleted"}) } // ============================================================================ // Contract CRUD // ============================================================================ // CreateContract creates a new contract for a vendor // POST /sdk/v1/vendors/contracts func (h *VendorHandlers) CreateContract(c *gin.Context) { var req vendor.CreateContractRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) contract := &vendor.Contract{ TenantID: tenantID, VendorID: req.VendorID, FileName: req.FileName, OriginalName: req.OriginalName, MimeType: req.MimeType, FileSize: req.FileSize, StoragePath: req.StoragePath, DocumentType: req.DocumentType, Parties: req.Parties, EffectiveDate: req.EffectiveDate, ExpirationDate: req.ExpirationDate, AutoRenewal: req.AutoRenewal, RenewalNoticePeriod: req.RenewalNoticePeriod, Version: req.Version, PreviousVersionID: req.PreviousVersionID, ReviewStatus: "PENDING", CreatedBy: userID.String(), } if contract.Version == "" { contract.Version = "1.0" } if err := h.store.CreateContract(c.Request.Context(), contract); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"contract": contract}) } // ListContracts lists contracts for a tenant // GET /sdk/v1/vendors/contracts func (h *VendorHandlers) ListContracts(c *gin.Context) { tenantID := rbac.GetTenantID(c) var vendorID *string if vid := c.Query("vendor_id"); vid != "" { vendorID = &vid } contracts, err := h.store.ListContracts(c.Request.Context(), tenantID.String(), vendorID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "contracts": contracts, "total": len(contracts), }) } // GetContract retrieves a contract by ID // GET /sdk/v1/vendors/contracts/:id func (h *VendorHandlers) GetContract(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") contract, err := h.store.GetContract(c.Request.Context(), tenantID.String(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if contract == nil { c.JSON(http.StatusNotFound, gin.H{"error": "contract not found"}) return } c.JSON(http.StatusOK, gin.H{"contract": contract}) } // UpdateContract updates a contract // PUT /sdk/v1/vendors/contracts/:id func (h *VendorHandlers) UpdateContract(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") contract, err := h.store.GetContract(c.Request.Context(), tenantID.String(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if contract == nil { c.JSON(http.StatusNotFound, gin.H{"error": "contract not found"}) return } var req vendor.UpdateContractRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.DocumentType != nil { contract.DocumentType = *req.DocumentType } if req.Parties != nil { contract.Parties = req.Parties } if req.EffectiveDate != nil { contract.EffectiveDate = req.EffectiveDate } if req.ExpirationDate != nil { contract.ExpirationDate = req.ExpirationDate } if req.AutoRenewal != nil { contract.AutoRenewal = *req.AutoRenewal } if req.RenewalNoticePeriod != nil { contract.RenewalNoticePeriod = *req.RenewalNoticePeriod } if req.ReviewStatus != nil { contract.ReviewStatus = *req.ReviewStatus } if req.ReviewCompletedAt != nil { contract.ReviewCompletedAt = req.ReviewCompletedAt } if req.ComplianceScore != nil { contract.ComplianceScore = req.ComplianceScore } if req.Version != nil { contract.Version = *req.Version } if req.ExtractedText != nil { contract.ExtractedText = *req.ExtractedText } if req.PageCount != nil { contract.PageCount = req.PageCount } if err := h.store.UpdateContract(c.Request.Context(), contract); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"contract": contract}) } // DeleteContract deletes a contract // DELETE /sdk/v1/vendors/contracts/:id func (h *VendorHandlers) DeleteContract(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") if err := h.store.DeleteContract(c.Request.Context(), tenantID.String(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "contract deleted"}) } // ============================================================================ // Finding CRUD // ============================================================================ // CreateFinding creates a new compliance finding // POST /sdk/v1/vendors/findings func (h *VendorHandlers) CreateFinding(c *gin.Context) { var req vendor.CreateFindingRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID := rbac.GetTenantID(c) finding := &vendor.Finding{ TenantID: tenantID, VendorID: req.VendorID, ContractID: req.ContractID, FindingType: req.FindingType, Category: req.Category, Severity: req.Severity, Title: req.Title, Description: req.Description, Recommendation: req.Recommendation, Citations: req.Citations, Status: vendor.FindingStatusOpen, Assignee: req.Assignee, DueDate: req.DueDate, } if err := h.store.CreateFinding(c.Request.Context(), finding); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"finding": finding}) } // ListFindings lists findings for a tenant // GET /sdk/v1/vendors/findings func (h *VendorHandlers) ListFindings(c *gin.Context) { tenantID := rbac.GetTenantID(c) var vendorID, contractID *string if vid := c.Query("vendor_id"); vid != "" { vendorID = &vid } if cid := c.Query("contract_id"); cid != "" { contractID = &cid } findings, err := h.store.ListFindings(c.Request.Context(), tenantID.String(), vendorID, contractID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "findings": findings, "total": len(findings), }) } // GetFinding retrieves a finding by ID // GET /sdk/v1/vendors/findings/:id func (h *VendorHandlers) GetFinding(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") finding, err := h.store.GetFinding(c.Request.Context(), tenantID.String(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if finding == nil { c.JSON(http.StatusNotFound, gin.H{"error": "finding not found"}) return } c.JSON(http.StatusOK, gin.H{"finding": finding}) } // UpdateFinding updates a finding // PUT /sdk/v1/vendors/findings/:id func (h *VendorHandlers) UpdateFinding(c *gin.Context) { tenantID := rbac.GetTenantID(c) id := c.Param("id") finding, err := h.store.GetFinding(c.Request.Context(), tenantID.String(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if finding == nil { c.JSON(http.StatusNotFound, gin.H{"error": "finding not found"}) return } var req vendor.UpdateFindingRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.FindingType != nil { finding.FindingType = *req.FindingType } if req.Category != nil { finding.Category = *req.Category } if req.Severity != nil { finding.Severity = *req.Severity } if req.Title != nil { finding.Title = *req.Title } if req.Description != nil { finding.Description = *req.Description } if req.Recommendation != nil { finding.Recommendation = *req.Recommendation } if req.Citations != nil { finding.Citations = req.Citations } if req.Status != nil { finding.Status = *req.Status } if req.Assignee != nil { finding.Assignee = *req.Assignee } if req.DueDate != nil { finding.DueDate = req.DueDate } if req.Resolution != nil { finding.Resolution = *req.Resolution } if err := h.store.UpdateFinding(c.Request.Context(), finding); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"finding": finding}) } // ResolveFinding resolves a finding with a resolution description // POST /sdk/v1/vendors/findings/:id/resolve func (h *VendorHandlers) ResolveFinding(c *gin.Context) { tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) id := c.Param("id") var req vendor.ResolveFindingRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.store.ResolveFinding(c.Request.Context(), tenantID.String(), id, req.Resolution, userID.String()); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "finding resolved"}) } // ============================================================================ // Control Instance Operations // ============================================================================ // UpsertControlInstance creates or updates a control instance // POST /sdk/v1/vendors/controls func (h *VendorHandlers) UpsertControlInstance(c *gin.Context) { var req struct { VendorID string `json:"vendor_id" binding:"required"` ControlID string `json:"control_id" binding:"required"` ControlDomain string `json:"control_domain"` Status vendor.ControlStatus `json:"status" binding:"required"` EvidenceIDs json.RawMessage `json:"evidence_ids,omitempty"` Notes string `json:"notes,omitempty"` NextAssessmentDate *time.Time `json:"next_assessment_date,omitempty"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) now := time.Now().UTC() userIDStr := userID.String() ci := &vendor.ControlInstance{ TenantID: tenantID, ControlID: req.ControlID, ControlDomain: req.ControlDomain, Status: req.Status, EvidenceIDs: req.EvidenceIDs, Notes: req.Notes, LastAssessedAt: &now, LastAssessedBy: &userIDStr, NextAssessmentDate: req.NextAssessmentDate, } // Parse VendorID vendorUUID, err := parseUUID(req.VendorID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid vendor_id"}) return } ci.VendorID = vendorUUID if err := h.store.UpsertControlInstance(c.Request.Context(), ci); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"control_instance": ci}) } // ListControlInstances lists control instances for a vendor // GET /sdk/v1/vendors/controls func (h *VendorHandlers) ListControlInstances(c *gin.Context) { vendorID := c.Query("vendor_id") if vendorID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "vendor_id query parameter is required"}) return } tenantID := rbac.GetTenantID(c) instances, err := h.store.ListControlInstances(c.Request.Context(), tenantID.String(), vendorID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "control_instances": instances, "total": len(instances), }) } // ============================================================================ // Template Operations // ============================================================================ // ListTemplates lists available templates // GET /sdk/v1/vendors/templates func (h *VendorHandlers) ListTemplates(c *gin.Context) { templateType := c.DefaultQuery("type", "VENDOR") var category, industry *string if cat := c.Query("category"); cat != "" { category = &cat } if ind := c.Query("industry"); ind != "" { industry = &ind } templates, err := h.store.ListTemplates(c.Request.Context(), templateType, category, industry) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "templates": templates, "total": len(templates), }) } // GetTemplate retrieves a template by its template_id string // GET /sdk/v1/vendors/templates/:templateId func (h *VendorHandlers) GetTemplate(c *gin.Context) { templateID := c.Param("templateId") if templateID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "template ID is required"}) return } tmpl, err := h.store.GetTemplate(c.Request.Context(), templateID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if tmpl == nil { c.JSON(http.StatusNotFound, gin.H{"error": "template not found"}) return } c.JSON(http.StatusOK, gin.H{"template": tmpl}) } // CreateTemplate creates a custom template // POST /sdk/v1/vendors/templates func (h *VendorHandlers) CreateTemplate(c *gin.Context) { var req vendor.CreateTemplateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tmpl := &vendor.Template{ TemplateType: req.TemplateType, TemplateID: req.TemplateID, Category: req.Category, NameDE: req.NameDE, NameEN: req.NameEN, DescriptionDE: req.DescriptionDE, DescriptionEN: req.DescriptionEN, TemplateData: req.TemplateData, Industry: req.Industry, Tags: req.Tags, IsSystem: req.IsSystem, IsActive: true, } // Set tenant for custom (non-system) templates if !req.IsSystem { tid := rbac.GetTenantID(c).String() tmpl.TenantID = &tid } if err := h.store.CreateTemplate(c.Request.Context(), tmpl); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"template": tmpl}) } // ApplyTemplate creates a vendor from a template // POST /sdk/v1/vendors/templates/:templateId/apply func (h *VendorHandlers) ApplyTemplate(c *gin.Context) { templateID := c.Param("templateId") if templateID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "template ID is required"}) return } tmpl, err := h.store.GetTemplate(c.Request.Context(), templateID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if tmpl == nil { c.JSON(http.StatusNotFound, gin.H{"error": "template not found"}) return } // Parse template_data to extract suggested vendor fields var templateData struct { ServiceCategory string `json:"service_category"` SuggestedRole string `json:"suggested_role"` DataAccessLevel string `json:"data_access_level"` ReviewFrequency string `json:"review_frequency"` Certifications json.RawMessage `json:"certifications"` ProcessingLocations json.RawMessage `json:"processing_locations"` } if err := json.Unmarshal(tmpl.TemplateData, &templateData); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse template data"}) return } // Optional overrides from request body var overrides struct { Name string `json:"name"` Country string `json:"country"` Website string `json:"website"` ContactName string `json:"contact_name"` ContactEmail string `json:"contact_email"` } c.ShouldBindJSON(&overrides) tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) v := &vendor.Vendor{ TenantID: tenantID, Name: overrides.Name, Country: overrides.Country, Website: overrides.Website, ContactName: overrides.ContactName, ContactEmail: overrides.ContactEmail, Role: vendor.VendorRole(templateData.SuggestedRole), ServiceCategory: templateData.ServiceCategory, DataAccessLevel: templateData.DataAccessLevel, ReviewFrequency: templateData.ReviewFrequency, Certifications: templateData.Certifications, ProcessingLocations: templateData.ProcessingLocations, Status: vendor.VendorStatusActive, TemplateID: &templateID, CreatedBy: userID.String(), } if v.Name == "" { v.Name = tmpl.NameDE } if v.Country == "" { v.Country = "DE" } if v.Role == "" { v.Role = vendor.VendorRoleProcessor } if err := h.store.CreateVendor(c.Request.Context(), v); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Increment template usage _ = h.store.IncrementTemplateUsage(c.Request.Context(), templateID) c.JSON(http.StatusCreated, gin.H{ "vendor": v, "template_id": templateID, "message": "vendor created from template", }) } // ============================================================================ // Statistics // ============================================================================ // GetStatistics returns aggregated vendor statistics // GET /sdk/v1/vendors/stats func (h *VendorHandlers) GetStatistics(c *gin.Context) { tenantID := rbac.GetTenantID(c) stats, err := h.store.GetVendorStats(c.Request.Context(), tenantID.String()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // ============================================================================ // Helpers // ============================================================================ func parseUUID(s string) (uuid.UUID, error) { return uuid.Parse(s) }