Files
2026-05-07 10:16:46 +08:00

139 lines
4.1 KiB
Go

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)
}