package handlers import ( "encoding/json" "net/http" "os" "path/filepath" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // PaymentHandlers handles payment compliance endpoints type PaymentHandlers struct { pool *pgxpool.Pool controls *PaymentControlLibrary } // PaymentControlLibrary holds the control catalog type PaymentControlLibrary struct { Domains []PaymentDomain `json:"domains"` Controls []PaymentControl `json:"controls"` } type PaymentDomain struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } type PaymentControl struct { ControlID string `json:"control_id"` Domain string `json:"domain"` Title string `json:"title"` Objective string `json:"objective"` CheckTarget string `json:"check_target"` Evidence []string `json:"evidence"` Automation string `json:"automation"` } type PaymentAssessment struct { ID uuid.UUID `json:"id"` TenantID uuid.UUID `json:"tenant_id"` ProjectName string `json:"project_name"` TenderReference string `json:"tender_reference,omitempty"` CustomerName string `json:"customer_name,omitempty"` Description string `json:"description,omitempty"` SystemType string `json:"system_type,omitempty"` PaymentMethods json.RawMessage `json:"payment_methods,omitempty"` Protocols json.RawMessage `json:"protocols,omitempty"` TotalControls int `json:"total_controls"` ControlsPassed int `json:"controls_passed"` ControlsFailed int `json:"controls_failed"` ControlsPartial int `json:"controls_partial"` ControlsNA int `json:"controls_not_applicable"` ControlsUnchecked int `json:"controls_not_checked"` ComplianceScore float64 `json:"compliance_score"` Status string `json:"status"` ControlResults json.RawMessage `json:"control_results,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` CreatedBy string `json:"created_by,omitempty"` } // NewPaymentHandlers creates payment handlers with loaded control library func NewPaymentHandlers(pool *pgxpool.Pool) *PaymentHandlers { lib := loadControlLibrary() return &PaymentHandlers{pool: pool, controls: lib} } func loadControlLibrary() *PaymentControlLibrary { // Try to load from policies directory paths := []string{ "policies/payment_controls_v1.json", "/app/policies/payment_controls_v1.json", } for _, p := range paths { data, err := os.ReadFile(p) if err != nil { // Try relative to executable execDir, _ := os.Executable() altPath := filepath.Join(filepath.Dir(execDir), p) data, err = os.ReadFile(altPath) if err != nil { continue } } var lib PaymentControlLibrary if err := json.Unmarshal(data, &lib); err == nil { return &lib } } return &PaymentControlLibrary{} } // ListControls returns the control library func (h *PaymentHandlers) ListControls(c *gin.Context) { domain := c.Query("domain") automation := c.Query("automation") controls := h.controls.Controls if domain != "" { var filtered []PaymentControl for _, ctrl := range controls { if ctrl.Domain == domain { filtered = append(filtered, ctrl) } } controls = filtered } if automation != "" { var filtered []PaymentControl for _, ctrl := range controls { if ctrl.Automation == automation { filtered = append(filtered, ctrl) } } controls = filtered } c.JSON(http.StatusOK, gin.H{ "controls": controls, "domains": h.controls.Domains, "total": len(controls), }) } // CreateAssessment creates a new payment compliance assessment func (h *PaymentHandlers) CreateAssessment(c *gin.Context) { var req PaymentAssessment if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID")) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } req.ID = uuid.New() req.TenantID = tenantID req.Status = "draft" req.TotalControls = len(h.controls.Controls) req.ControlsUnchecked = req.TotalControls req.CreatedAt = time.Now() req.UpdatedAt = time.Now() _, err := h.pool.Exec(c.Request.Context(), ` INSERT INTO payment_compliance_assessments ( id, tenant_id, project_name, tender_reference, customer_name, description, system_type, payment_methods, protocols, total_controls, controls_not_checked, status, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, req.ID, req.TenantID, req.ProjectName, req.TenderReference, req.CustomerName, req.Description, req.SystemType, req.PaymentMethods, req.Protocols, req.TotalControls, req.ControlsUnchecked, req.Status, req.CreatedBy, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, req) } // ListAssessments lists all payment assessments for a tenant func (h *PaymentHandlers) ListAssessments(c *gin.Context) { tenantID, _ := uuid.Parse(c.GetHeader("X-Tenant-ID")) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } rows, err := h.pool.Query(c.Request.Context(), ` SELECT id, tenant_id, project_name, tender_reference, customer_name, system_type, total_controls, controls_passed, controls_failed, controls_partial, controls_not_applicable, controls_not_checked, compliance_score, status, created_at, updated_at FROM payment_compliance_assessments WHERE tenant_id = $1 ORDER BY created_at DESC`, tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var assessments []PaymentAssessment for rows.Next() { var a PaymentAssessment rows.Scan(&a.ID, &a.TenantID, &a.ProjectName, &a.TenderReference, &a.CustomerName, &a.SystemType, &a.TotalControls, &a.ControlsPassed, &a.ControlsFailed, &a.ControlsPartial, &a.ControlsNA, &a.ControlsUnchecked, &a.ComplianceScore, &a.Status, &a.CreatedAt, &a.UpdatedAt) assessments = append(assessments, a) } if assessments == nil { assessments = []PaymentAssessment{} } c.JSON(http.StatusOK, gin.H{"assessments": assessments, "total": len(assessments)}) } // GetAssessment returns a single assessment with control results func (h *PaymentHandlers) GetAssessment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } var a PaymentAssessment err = h.pool.QueryRow(c.Request.Context(), ` SELECT id, tenant_id, project_name, tender_reference, customer_name, description, system_type, payment_methods, protocols, total_controls, controls_passed, controls_failed, controls_partial, controls_not_applicable, controls_not_checked, compliance_score, status, control_results, created_at, updated_at, created_by FROM payment_compliance_assessments WHERE id = $1`, id).Scan( &a.ID, &a.TenantID, &a.ProjectName, &a.TenderReference, &a.CustomerName, &a.Description, &a.SystemType, &a.PaymentMethods, &a.Protocols, &a.TotalControls, &a.ControlsPassed, &a.ControlsFailed, &a.ControlsPartial, &a.ControlsNA, &a.ControlsUnchecked, &a.ComplianceScore, &a.Status, &a.ControlResults, &a.CreatedAt, &a.UpdatedAt, &a.CreatedBy) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "assessment not found"}) return } c.JSON(http.StatusOK, a) } // UpdateControlVerdict updates the verdict for a single control func (h *PaymentHandlers) UpdateControlVerdict(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } var body struct { ControlID string `json:"control_id"` Verdict string `json:"verdict"` // passed, failed, partial, na, unchecked Evidence string `json:"evidence,omitempty"` Notes string `json:"notes,omitempty"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update the control_results JSONB and recalculate scores _, err = h.pool.Exec(c.Request.Context(), ` WITH updated AS ( SELECT id, COALESCE(control_results, '[]'::jsonb) AS existing_results FROM payment_compliance_assessments WHERE id = $1 ) UPDATE payment_compliance_assessments SET control_results = ( SELECT jsonb_agg( CASE WHEN elem->>'control_id' = $2 THEN jsonb_build_object('control_id', $2, 'verdict', $3, 'evidence', $4, 'notes', $5) ELSE elem END ) FROM updated, jsonb_array_elements( CASE WHEN existing_results @> jsonb_build_array(jsonb_build_object('control_id', $2)) THEN existing_results ELSE existing_results || jsonb_build_array(jsonb_build_object('control_id', $2, 'verdict', $3, 'evidence', $4, 'notes', $5)) END ) AS elem ), updated_at = NOW() WHERE id = $1`, id, body.ControlID, body.Verdict, body.Evidence, body.Notes) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "updated", "control_id": body.ControlID, "verdict": body.Verdict}) }