feat: Add Academy, Whistleblower, Incidents, Vendor, DSB, SSO, Reporting, Multi-Tenant and Industry backends
Go handlers, models, stores and migrations for all SDK modules. Updates developer portal navigation and BYOEH page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
850
ai-compliance-sdk/internal/api/handlers/vendor_handlers.go
Normal file
850
ai-compliance-sdk/internal/api/handlers/vendor_handlers.go
Normal file
@@ -0,0 +1,850 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user