Files
2026-05-12 18:49:52 +08:00

99 lines
2.7 KiB
Go

package probe
import (
"context"
"time"
"supply-intelligence/internal/domain"
"supply-intelligence/internal/metrics"
)
type RoutingStateRepository interface {
GetRoutingStateContext(ctx context.Context, accountID int64) (domain.AccountRoutingState, bool)
UpsertRoutingStateContext(ctx context.Context, state domain.AccountRoutingState) domain.AccountRoutingState
}
type Service struct {
repo RoutingStateRepository
now func() time.Time
}
type EvaluateInput struct {
AccountID int64
Platform string
CurrentStatus domain.AccountStatus
StatusCode int
TransportError error
ConsecutiveExplicitFailures int
}
type EvaluateOutput struct {
Classification domain.ProbeClassification `json:"classification"`
ReasonCode string `json:"reason_code"`
RoutingState domain.AccountRoutingState `json:"routing_state"`
}
func NewService(repo RoutingStateRepository) *Service {
return &Service{
repo: repo,
now: func() time.Time {
return time.Now().UTC()
},
}
}
func (s *Service) EvaluateHTTPResult(ctx context.Context, input EvaluateInput) (EvaluateOutput, error) {
classification, reasonCode, err := ClassifyHTTPResult(input.StatusCode, input.TransportError)
metrics.ProbeEvaluationsTotal.WithLabelValues(input.Platform, string(classification)).Inc()
if err != nil {
return EvaluateOutput{}, err
}
observedAt := s.now()
nextStatus := NextAccountStatus(input.CurrentStatus, classification, input.ConsecutiveExplicitFailures)
state := domain.AccountRoutingState{
AccountID: input.AccountID,
Platform: input.Platform,
AccountStatus: nextStatus,
RoutingEnabled: nextStatus == domain.AccountStatusActive,
RiskScore: riskScoreFor(nextStatus, classification),
ReasonCode: reasonCode,
LastProbeAt: observedAt,
Version: 1,
}
if previous, ok := s.repo.GetRoutingStateContext(ctx, input.AccountID); ok {
state.Version = previous.Version + 1
if state.Platform == "" {
state.Platform = previous.Platform
}
}
persisted := s.repo.UpsertRoutingStateContext(ctx, state)
return EvaluateOutput{
Classification: classification,
ReasonCode: reasonCode,
RoutingState: persisted,
}, nil
}
func riskScoreFor(status domain.AccountStatus, classification domain.ProbeClassification) int {
switch classification {
case domain.ProbeClassificationSuccess:
return 20
case domain.ProbeClassificationExplicitFailure:
switch status {
case domain.AccountStatusDisabled:
return 100
case domain.AccountStatusSuspended:
return 90
default:
return 80
}
case domain.ProbeClassificationInconclusive:
return 60
default:
return 0
}
}