package probe import ( "context" "log" "time" "github.com/google/uuid" "supply-intelligence/internal/domain" "supply-intelligence/internal/integration" ) // ProbeLogRepository defines where probe execution logs are persisted type ProbeLogRepository interface { AppendProbeLog(ctx context.Context, outcome ProbeOutcome) error } // Driver orchestrates a full probe run: load targets → execute → evaluate → persist state type Driver struct { executor *ProbeExecutor evaluator *Service // reuse the existing probe.Service as evaluator logRepo ProbeLogRepository adapters map[string]integration.SupplierAdapter now func() time.Time } // NewDriver creates a probe driver with all dependencies wired together func NewDriver( repo RoutingStateRepository, logRepo ProbeLogRepository, adapters map[string]integration.SupplierAdapter, ) *Driver { return &Driver{ executor: NewProbeExecutor(integration.NewDefaultHTTPClient()), evaluator: NewService(repo), logRepo: logRepo, adapters: adapters, now: func() time.Time { return time.Now().UTC() }, } } // RunProbeForAccount probes a single account and persists the result through the full chain func (d *Driver) RunProbeForAccount(ctx context.Context, account integration.SupplierAccount) error { var outcome ProbeOutcome if adapter, ok := d.adapters[account.Platform]; ok { // Use platform-specific adapter result := adapter.ProbeAccount(ctx, account) outcome = ProbeOutcome{ AccountID: account.AccountID, Platform: account.Platform, StatusCode: result.StatusCode, TransportError: result.TransportError, ResponseBody: result.ResponseBody, RequestID: "prb-" + uuid.New().String(), ExecutedAt: d.now(), } } else { // Fall back to generic HTTP probe target := ProbeTarget{ AccountID: account.AccountID, Platform: account.Platform, Endpoint: account.Endpoint, AuthHeader: "Bearer " + account.APIKey, } if target.Endpoint == "" { target.Endpoint = account.BaseURL } var err error outcome, err = d.executor.ExecuteProbe(ctx, target) if err != nil { return err } } return d.persistOutcome(ctx, account.AccountID, account.Platform, outcome) } // persistOutcome drives the outcome through: load current state → evaluate → state machine → persist func (d *Driver) persistOutcome(ctx context.Context, accountID int64, platform string, outcome ProbeOutcome) error { // 1. Load current routing state currentState, _ := d.evaluator.repo.GetRoutingStateContext(ctx, accountID) // 2. Build evaluate input var transportErr error if outcome.TransportError != nil { transportErr = outcome.TransportError } input := EvaluateInput{ AccountID: accountID, Platform: platform, CurrentStatus: currentState.AccountStatus, StatusCode: outcome.StatusCode, TransportError: transportErr, } // 3. Evaluate (uses the existing Service.EvaluateHTTPResult) evalOutput, err := d.evaluator.EvaluateHTTPResult(ctx, input) if err != nil { log.Printf("[probe] failed to evaluate outcome for account %d: %v", accountID, err) return err } // 4. Log the probe execution if d.logRepo != nil { logEntry := ProbeOutcome{ AccountID: accountID, Platform: platform, StatusCode: outcome.StatusCode, TransportError: outcome.TransportError, LatencyMs: outcome.LatencyMs, RequestID: outcome.RequestID, ExecutedAt: outcome.ExecutedAt, } _ = d.logRepo.AppendProbeLog(ctx, logEntry) } // 5. Log state transition transition := describeTransition(currentState.AccountStatus, evalOutput.RoutingState.AccountStatus) log.Printf("[probe] account=%d platform=%s %s->%s classification=%s risk=%d transition=%s", accountID, platform, currentState.AccountStatus, evalOutput.RoutingState.AccountStatus, evalOutput.Classification, evalOutput.RoutingState.RiskScore, transition) return nil } // describeTransition returns a human-readable transition description func describeTransition(from, to domain.AccountStatus) string { if from == to { return "no_change" } return string(from) + "_to_" + string(to) }