package handoff import ( "context" "fmt" "time" ) type Manager struct { Config Config Store *Store Executor Executor } type RequestInput struct { CallID string AsteriskChannelID string State string Language string RegionCode string Route string ReasonCode HandoffReasonCode ReasonText string Summary string Metadata map[string]string } func NewManager(cfg Config, executor Executor) *Manager { if cfg.Mode == "" { cfg = DefaultConfig() } if executor == nil { executor = SafeExecutor{} } return &Manager{Config: cfg, Store: NewStore(), Executor: executor} } func (m *Manager) Request(ctx context.Context, in RequestInput) (HandoffRequest, HandoffResult, error) { if existing, ok := m.Store.GetByCall(in.CallID); ok { result := HandoffResult{RequestID: existing.ID, Status: existing.Status, Mode: existing.Mode, MessageKey: "handoff.already_requested", Message: Message("handoff.already_requested", in.Language)} return existing, result, nil } mode := m.Config.Mode hasTarget := mode == HandoffModeHangupAfterMessage || mode == HandoffModeDisabledStub || (mode == HandoffModeARIRedirect && m.Config.TargetEndpoint != "") || (mode == HandoffModeDialplanContinue && m.Config.DialplanContext != "" && m.Config.DialplanExtension != "") decision := Authorize(PolicyContext{State: in.State, Route: in.Route, HandoffEnabled: m.Config.Enabled, HandoffMode: mode, AllowInTestRouteOnly: m.Config.AllowInTestRouteOnly, HasTarget: hasTarget}) now := time.Now().UTC() req := HandoffRequest{ ID: fmt.Sprintf("handoff-%d", now.UnixNano()), CallID: in.CallID, AsteriskChannelID: in.AsteriskChannelID, Mode: mode, Status: HandoffStatusRequested, ReasonCode: in.ReasonCode, ReasonText: SanitizeText(in.ReasonText, m.Config.MaxSummaryChars), Summary: SanitizeText(in.Summary, m.Config.MaxSummaryChars), Language: in.Language, RegionCode: in.RegionCode, Route: in.Route, TargetEndpoint: m.Config.TargetEndpoint, DialplanContext: m.Config.DialplanContext, DialplanExtension: m.Config.DialplanExtension, DialplanPriority: m.Config.DialplanPriority, CreatedAt: now, UpdatedAt: now, Metadata: in.Metadata, } if !decision.Allowed { req.Status = HandoffStatusFailed result := HandoffResult{RequestID: req.ID, Status: req.Status, Mode: req.Mode, MessageKey: decision.MessageKey, Message: Message(decision.MessageKey, in.Language), Error: decision.ReasonCode} m.Store.Put(req) return req, result, fmt.Errorf(decision.ReasonCode) } if decision.StubOnly { req.Mode = HandoffModeDisabledStub req.Status = HandoffStatusStubbed result := HandoffResult{RequestID: req.ID, Status: req.Status, Mode: req.Mode, MessageKey: decision.MessageKey, Message: Message(decision.MessageKey, in.Language)} m.Store.Put(req) return req, result, nil } result, err := m.Executor.Execute(ctx, req) req.Status = result.Status req.Error = result.Error completed := time.Now().UTC() if result.Status == HandoffStatusCompleted || result.Status == HandoffStatusFailed || result.Status == HandoffStatusStubbed { req.CompletedAt = &completed } req.UpdatedAt = completed m.Store.Put(req) return req, result, err }