package approval import ( "fmt" "sync" "time" ) // ApprovalStatus represents approval state type ApprovalStatus string const ( StatusPending ApprovalStatus = "pending" StatusApproved ApprovalStatus = "approved" StatusRejected ApprovalStatus = "rejected" StatusExpired ApprovalStatus = "expired" ) // ApprovalDecision represents an approval decision type ApprovalDecision struct { Status ApprovalStatus `json:"status"` ApprovedBy string `json:"approved_by"` RejectedBy string `json:"rejected_by"` Reason string `json:"reason"` Comments string `json:"comments"` Timestamp time.Time `json:"timestamp"` ExpiresAt time.Time `json:"expires_at"` } // ApprovalGate represents a human approval gate type ApprovalGate struct { ID string TaskID string WorkflowID string Description string Decision *ApprovalDecision CreatedAt time.Time TTL time.Duration // Time until gate expires RequiredApprovals int // Number of approvals needed (1 or more) Approvals []string // List of approvers } // ApprovalGateManager manages approval gates type ApprovalGateManager struct { mu sync.RWMutex gates map[string]*ApprovalGate decisions map[string]*ApprovalDecision history []*ApprovalRecord stats *ApprovalStats } // ApprovalRecord tracks approval history type ApprovalRecord struct { GateID string Decision ApprovalStatus ApprovedBy string RejectedBy string Timestamp time.Time Reason string } // ApprovalStats tracks approval statistics type ApprovalStats struct { TotalGates int ApprovedGates int RejectedGates int PendingGates int ExpiredGates int AverageWaitTime time.Duration } // NewApprovalGateManager creates a new approval gate manager func NewApprovalGateManager() *ApprovalGateManager { return &ApprovalGateManager{ gates: make(map[string]*ApprovalGate), decisions: make(map[string]*ApprovalDecision), history: make([]*ApprovalRecord, 0), stats: &ApprovalStats{ AverageWaitTime: 0, }, } } // CreateGate creates a new approval gate func (agm *ApprovalGateManager) CreateGate(taskID, workflowID, description string, ttl time.Duration) *ApprovalGate { if ttl == 0 { ttl = 24 * time.Hour // Default 24 hours } gate := &ApprovalGate{ ID: fmt.Sprintf("gate-%d", time.Now().UnixNano()), TaskID: taskID, WorkflowID: workflowID, Description: description, CreatedAt: time.Now(), TTL: ttl, RequiredApprovals: 1, Approvals: make([]string, 0), Decision: &ApprovalDecision{ Status: StatusPending, ExpiresAt: time.Now().Add(ttl), }, } agm.mu.Lock() defer agm.mu.Unlock() agm.gates[gate.ID] = gate agm.decisions[gate.ID] = gate.Decision agm.stats.TotalGates++ agm.stats.PendingGates++ return gate } // ApproveGate approves a gate func (agm *ApprovalGateManager) ApproveGate(gateID, approvedBy, comments string) error { agm.mu.Lock() defer agm.mu.Unlock() gate, exists := agm.gates[gateID] if !exists { return fmt.Errorf("gate not found: %s", gateID) } if gate.Decision.Status == StatusApproved || gate.Decision.Status == StatusRejected { return fmt.Errorf("gate already has a decision: %s", gate.Decision.Status) } if time.Now().After(gate.Decision.ExpiresAt) { gate.Decision.Status = StatusExpired agm.stats.ExpiredGates++ agm.stats.PendingGates-- return fmt.Errorf("gate has expired") } gate.Decision.Status = StatusApproved gate.Decision.ApprovedBy = approvedBy gate.Decision.Comments = comments gate.Decision.Timestamp = time.Now() gate.Approvals = append(gate.Approvals, approvedBy) // Record in history record := &ApprovalRecord{ GateID: gateID, Decision: StatusApproved, ApprovedBy: approvedBy, Timestamp: gate.Decision.Timestamp, Reason: comments, } agm.history = append(agm.history, record) agm.stats.ApprovedGates++ agm.stats.PendingGates-- return nil } // RejectGate rejects a gate func (agm *ApprovalGateManager) RejectGate(gateID, rejectedBy, reason string) error { agm.mu.Lock() defer agm.mu.Unlock() gate, exists := agm.gates[gateID] if !exists { return fmt.Errorf("gate not found: %s", gateID) } if gate.Decision.Status == StatusApproved || gate.Decision.Status == StatusRejected { return fmt.Errorf("gate already has a decision: %s", gate.Decision.Status) } if time.Now().After(gate.Decision.ExpiresAt) { gate.Decision.Status = StatusExpired agm.stats.ExpiredGates++ agm.stats.PendingGates-- return fmt.Errorf("gate has expired") } gate.Decision.Status = StatusRejected gate.Decision.RejectedBy = rejectedBy gate.Decision.Reason = reason gate.Decision.Timestamp = time.Now() // Record in history record := &ApprovalRecord{ GateID: gateID, Decision: StatusRejected, RejectedBy: rejectedBy, Timestamp: gate.Decision.Timestamp, Reason: reason, } agm.history = append(agm.history, record) agm.stats.RejectedGates++ agm.stats.PendingGates-- return nil } // GetGate retrieves a gate func (agm *ApprovalGateManager) GetGate(gateID string) (*ApprovalGate, bool) { agm.mu.RLock() defer agm.mu.RUnlock() gate, exists := agm.gates[gateID] return gate, exists } // GetDecision retrieves a decision func (agm *ApprovalGateManager) GetDecision(gateID string) (*ApprovalDecision, bool) { agm.mu.RLock() defer agm.mu.RUnlock() decision, exists := agm.decisions[gateID] return decision, exists } // GetPendingGates returns all pending gates func (agm *ApprovalGateManager) GetPendingGates() []*ApprovalGate { agm.mu.RLock() defer agm.mu.RUnlock() pending := make([]*ApprovalGate, 0) for _, gate := range agm.gates { if gate.Decision.Status == StatusPending { // Check if expired if time.Now().After(gate.Decision.ExpiresAt) { gate.Decision.Status = StatusExpired } else { pending = append(pending, gate) } } } return pending } // GetGatesByTask returns all gates for a task func (agm *ApprovalGateManager) GetGatesByTask(taskID string) []*ApprovalGate { agm.mu.RLock() defer agm.mu.RUnlock() gates := make([]*ApprovalGate, 0) for _, gate := range agm.gates { if gate.TaskID == taskID { gates = append(gates, gate) } } return gates } // GetGatesByWorkflow returns all gates for a workflow func (agm *ApprovalGateManager) GetGatesByWorkflow(workflowID string) []*ApprovalGate { agm.mu.RLock() defer agm.mu.RUnlock() gates := make([]*ApprovalGate, 0) for _, gate := range agm.gates { if gate.WorkflowID == workflowID { gates = append(gates, gate) } } return gates } // IsApproved checks if a gate is approved func (agm *ApprovalGateManager) IsApproved(gateID string) bool { agm.mu.RLock() defer agm.mu.RUnlock() gate, exists := agm.gates[gateID] if !exists { return false } return gate.Decision.Status == StatusApproved } // IsRejected checks if a gate is rejected func (agm *ApprovalGateManager) IsRejected(gateID string) bool { agm.mu.RLock() defer agm.mu.RUnlock() gate, exists := agm.gates[gateID] if !exists { return false } return gate.Decision.Status == StatusRejected } // IsPending checks if a gate is still pending func (agm *ApprovalGateManager) IsPending(gateID string) bool { agm.mu.RLock() defer agm.mu.RUnlock() gate, exists := agm.gates[gateID] if !exists { return false } if time.Now().After(gate.Decision.ExpiresAt) { return false // Expired gates are not pending } return gate.Decision.Status == StatusPending } // GetStats returns statistics func (agm *ApprovalGateManager) GetStats() *ApprovalStats { agm.mu.RLock() defer agm.mu.RUnlock() stats := *agm.stats return &stats } // GetHistory returns approval history func (agm *ApprovalGateManager) GetHistory() []*ApprovalRecord { agm.mu.RLock() defer agm.mu.RUnlock() result := make([]*ApprovalRecord, len(agm.history)) copy(result, agm.history) return result } // Clear clears all gates func (agm *ApprovalGateManager) Clear() { agm.mu.Lock() defer agm.mu.Unlock() agm.gates = make(map[string]*ApprovalGate) agm.decisions = make(map[string]*ApprovalDecision) agm.history = make([]*ApprovalRecord, 0) agm.stats = &ApprovalStats{} }