This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/api/handlers/vendor_handlers.go
BreakPilot Dev 557305db5d
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
feat: Add Academy, Whistleblower, Incidents SDK modules, pitch-deck, blog and CI/CD config
- Academy, Whistleblower, Incidents frontend pages with API proxies and types
- Vendor compliance API proxy route
- Go backend handlers and models for all new SDK modules
- Investor pitch-deck app with interactive slides
- Blog section with DSGVO, AI Act, NIS2, glossary articles
- MkDocs documentation site
- CI/CD pipelines (Woodpecker, GitHub Actions), security scanning config
- Planning and implementation documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 21:12:16 +01:00

851 lines
24 KiB
Go

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)
}