package manager import ( "context" "encoding/json" "fmt" "time" "bugulma/backend/internal/domain" "bugulma/backend/internal/matching/engine" ) // Context keys for extracting user/org info type contextKey string const ( UserIDKey contextKey = "user_id" OrgIDKey contextKey = "org_id" ) // Helper functions to extract context values func getUserIDFromContext(ctx context.Context) string { if userID, ok := ctx.Value(UserIDKey).(string); ok { return userID } return "system" } func getOrgIDFromContext(ctx context.Context) string { if orgID, ok := ctx.Value(OrgIDKey).(string); ok { return orgID } return "" } // EventPublisher interface for publishing events type EventPublisher interface { Publish(event domain.Event) error } // Manager handles match lifecycle operations type Manager struct { matchRepo domain.MatchRepository negotiationRepo domain.NegotiationHistoryRepository eventBus EventPublisher } // NewManager creates a new lifecycle manager func NewManager( matchRepo domain.MatchRepository, negotiationRepo domain.NegotiationHistoryRepository, eventBus EventPublisher, ) *Manager { return &Manager{ matchRepo: matchRepo, negotiationRepo: negotiationRepo, eventBus: eventBus, } } // CreateMatch creates a new match from a candidate func (m *Manager) CreateMatch(ctx context.Context, candidate *engine.Candidate, creatorID string) (*domain.Match, error) { // Create match entity match := &domain.Match{ ID: generateMatchID(), SourceResourceID: candidate.SourceFlow.ID, TargetResourceID: candidate.TargetFlow.ID, Status: domain.MatchStatusSuggested, CompatibilityScore: candidate.CompatibilityScore, TemporalOverlapScore: candidate.TemporalScore, QualityScore: candidate.QualityScore, EconomicValue: candidate.EstimatedAnnualSavings, DistanceKm: candidate.DistanceKm, Priority: candidate.Priority, } // Set contract details (embedded) now := time.Now() terminationDate := now.Add(365 * 24 * time.Hour) // 1 year default match.ContractDetails = &domain.ContractDetails{ EffectiveFrom: &now, TerminationDate: &terminationDate, ValuePerYear: candidate.EstimatedAnnualSavings, } // Store economic data in JSONB field economicData, _ := json.Marshal(map[string]interface{}{ "annual_savings": candidate.EstimatedAnnualSavings, "npv": 0, "irr": 0, "payback_years": 0, "co2_avoided_tonnes": 0, "capex_required": 0, "opex_per_year": 0, }) match.EconomicImpact = economicData // Store risk data in JSONB field riskData, _ := json.Marshal(map[string]interface{}{ "technical_risk": 0.3, "regulatory_risk": 0.2, "market_risk": 0.1, "overall_risk": 0.2, "risk_level": candidate.RiskLevel, }) match.RiskAssessment = riskData // Create the match if err := m.matchRepo.Create(ctx, match); err != nil { return nil, fmt.Errorf("failed to create match: %w", err) } // Add initial negotiation entry negotiationEntry := &domain.NegotiationHistoryEntry{ MatchID: match.ID, Timestamp: time.Now(), ActorID: creatorID, Action: "created", Notes: "Match created from automated matching process", } if err := m.negotiationRepo.Create(ctx, negotiationEntry); err != nil { fmt.Printf("Warning: Failed to create negotiation history: %v\n", err) } // Publish event if m.eventBus != nil { event := domain.Event{ ID: generateEventID(), Type: domain.EventTypeMatchCreated, EntityID: match.ID, OrgID: getOrgIDFromContext(ctx), // Extract from request context UserID: creatorID, Timestamp: time.Now(), Payload: map[string]interface{}{ "match": match, }, } if err := m.eventBus.Publish(event); err != nil { fmt.Printf("Warning: Failed to publish match created event: %v\n", err) } } return match, nil } // UpdateMatchStatus updates the status of a match with proper validation func (m *Manager) UpdateMatchStatus( ctx context.Context, matchID string, newStatus domain.MatchStatus, actorID string, notes string, ) error { // Get current match match, err := m.matchRepo.GetByID(ctx, matchID) if err != nil { return fmt.Errorf("failed to get match: %w", err) } oldStatus := match.Status // Validate status transition if err := m.validateStatusTransition(oldStatus, newStatus); err != nil { return fmt.Errorf("invalid status transition: %w", err) } // Update match match.Status = newStatus // Handle status-specific logic switch newStatus { case domain.MatchStatusReserved: reservedUntil := time.Now().Add(24 * time.Hour) match.ReservedUntil = &reservedUntil case domain.MatchStatusCancelled: now := time.Now() match.CancelledAt = &now match.CancelledBy = actorID } if err := m.matchRepo.Update(ctx, match); err != nil { return fmt.Errorf("failed to update match: %w", err) } // Add negotiation history oldValue, _ := json.Marshal(string(oldStatus)) newValue, _ := json.Marshal(string(newStatus)) negotiationEntry := &domain.NegotiationHistoryEntry{ MatchID: matchID, Timestamp: time.Now(), ActorID: actorID, Action: "status_changed", Notes: fmt.Sprintf("Status changed from %s to %s. %s", oldStatus, newStatus, notes), OldValue: oldValue, NewValue: newValue, } if err := m.negotiationRepo.Create(ctx, negotiationEntry); err != nil { fmt.Printf("Warning: Failed to create negotiation history: %v\n", err) } // Publish event if m.eventBus != nil { event := domain.Event{ ID: generateEventID(), Type: domain.EventTypeMatchStatusChanged, EntityID: matchID, OrgID: getOrgIDFromContext(ctx), // Extract from request context UserID: actorID, Timestamp: time.Now(), Payload: map[string]interface{}{ "old_status": oldStatus, "new_status": newStatus, "match_id": matchID, }, } if err := m.eventBus.Publish(event); err != nil { fmt.Printf("Warning: Failed to publish status change event: %v\n", err) } } return nil } // UpdateMatch updates match details func (m *Manager) UpdateMatch(ctx context.Context, match *domain.Match) error { match.UpdatedAt = time.Now() if err := m.matchRepo.Update(ctx, match); err != nil { return fmt.Errorf("failed to update match: %w", err) } // Publish event if m.eventBus != nil { event := domain.Event{ ID: generateEventID(), Type: domain.EventTypeMatchUpdated, EntityID: match.ID, OrgID: getOrgIDFromContext(ctx), // Extract from request context UserID: getUserIDFromContext(ctx), // Extract from request context Timestamp: time.Now(), Payload: map[string]interface{}{ "match": match, }, } if err := m.eventBus.Publish(event); err != nil { fmt.Printf("Warning: Failed to publish match updated event: %v\n", err) } } return nil } // DeleteMatch marks a match as cancelled func (m *Manager) DeleteMatch(ctx context.Context, matchID string, actorID string) error { match, err := m.matchRepo.GetByID(ctx, matchID) if err != nil { return fmt.Errorf("failed to get match: %w", err) } // Mark as cancelled if not already in terminal state if match.Status != domain.MatchStatusFailed && match.Status != domain.MatchStatusCancelled { now := time.Now() match.Status = domain.MatchStatusCancelled match.CancelledAt = &now match.CancelledBy = actorID if err := m.matchRepo.Update(ctx, match); err != nil { return fmt.Errorf("failed to update match status: %w", err) } } return nil } // GetNegotiationHistory retrieves negotiation history for a match func (m *Manager) GetNegotiationHistory(ctx context.Context, matchID string) ([]*domain.NegotiationHistoryEntry, error) { return m.negotiationRepo.GetByMatchID(ctx, matchID) } // validateStatusTransition validates that a status transition is allowed func (m *Manager) validateStatusTransition(from, to domain.MatchStatus) error { validTransitions := map[domain.MatchStatus][]domain.MatchStatus{ domain.MatchStatusSuggested: { domain.MatchStatusNegotiating, domain.MatchStatusReserved, domain.MatchStatusFailed, domain.MatchStatusCancelled, }, domain.MatchStatusNegotiating: { domain.MatchStatusReserved, domain.MatchStatusContracted, domain.MatchStatusFailed, domain.MatchStatusCancelled, }, domain.MatchStatusReserved: { domain.MatchStatusContracted, domain.MatchStatusNegotiating, domain.MatchStatusFailed, domain.MatchStatusCancelled, }, domain.MatchStatusContracted: { domain.MatchStatusLive, domain.MatchStatusFailed, domain.MatchStatusCancelled, }, domain.MatchStatusLive: { domain.MatchStatusFailed, domain.MatchStatusCancelled, }, // Terminal states domain.MatchStatusFailed: {}, domain.MatchStatusCancelled: {}, } allowedTransitions, exists := validTransitions[from] if !exists { return fmt.Errorf("unknown status: %s", from) } for _, allowed := range allowedTransitions { if allowed == to { return nil } } return fmt.Errorf("invalid transition from %s to %s", from, to) } // generateMatchID generates a unique match ID func generateMatchID() string { return fmt.Sprintf("match_%d", time.Now().UnixNano()) } // generateNegotiationID generates a unique negotiation entry ID func generateNegotiationID() string { return fmt.Sprintf("neg_%d", time.Now().UnixNano()) } // generateEventID generates a unique event ID func generateEventID() string { return fmt.Sprintf("evt_%d", time.Now().UnixNano()) }