package app import ( "context" "fmt" "github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/ports" "github.com/google/uuid" ) // ApprovalService is the approval-decision use-case: list pending approvals // and decide (approve/deny/revoke), which un-gates the linked execution and // persists the audit trail. The driving adapter dispatches the SSH work for // approved executions after Decide returns. type ApprovalService struct { approvals ports.ApprovalRepository } // NewApprovalService wires the service. func NewApprovalService(approvals ports.ApprovalRepository) *ApprovalService { return &ApprovalService{approvals: approvals} } // List returns pending approvals for the given entity. func (s *ApprovalService) List(ctx context.Context, entityID domain.UUID, limit int) ([]domain.Approval, error) { return s.approvals.ListPending(ctx, entityID, limit) } // ApprovalDecideCmd is one decide-approval request. type ApprovalDecideCmd struct { ApprovalID domain.UUID Token string Decision string // "approve" | "deny" | "revoke" Actor string } // Decide validates the decision, maps to status, and delegates the // transactional flip + execution un-gate + audit + event to the repository. // Returns the updated approval and, on approve, the execution to resume. func (s *ApprovalService) Decide(ctx context.Context, cmd ApprovalDecideCmd) (ports.ApprovalDecideResult, error) { var status string switch cmd.Decision { case "approve": status = "approved" case "deny": status = "denied" case "revoke": status = "revoked" default: return ports.ApprovalDecideResult{}, fmt.Errorf("%w: invalid decision %q", domain.ErrInvalidInput, cmd.Decision) } aid, err := uuid.Parse(string(cmd.ApprovalID)) if err != nil { return ports.ApprovalDecideResult{}, fmt.Errorf("parse approval id: %w", err) } return s.approvals.Decide(ctx, ports.ApprovalDecideInput{ ApprovalID: domain.UUID(aid.String()), Token: cmd.Token, Status: status, Actor: cmd.Actor, }) }