package platform import ( "context" "net/url" "sort" "strings" ) type Store interface { DashboardSummary(context.Context) (DashboardSummary, error) Vehicles(context.Context, url.Values) (Page[VehicleRow], error) RealtimeLocations(context.Context, url.Values) (Page[RealtimeLocationRow], error) HistoryLocations(context.Context, url.Values) (Page[HistoryLocationRow], error) HistoryLocationsFromTDengine(context.Context, url.Values) (Page[HistoryLocationRow], error) RawFrames(context.Context, RawFrameQuery) (Page[RawFrameRow], error) DailyMileage(context.Context, url.Values) (Page[DailyMileageRow], error) QualityIssues(context.Context, url.Values) (Page[QualityIssueRow], error) OpsHealth(context.Context) (OpsHealth, error) } type RawFrameQuery struct { Protocol string `json:"protocol"` VIN string `json:"vin"` DateFrom string `json:"dateFrom"` DateTo string `json:"dateTo"` Fields []string `json:"fields"` IncludeFields bool `json:"includeFields"` Limit int `json:"limit"` Offset int `json:"offset"` } type Service struct { store Store } func NewService(store Store) *Service { return &Service{store: store} } func (s *Service) DashboardSummary(ctx context.Context) (DashboardSummary, error) { return s.store.DashboardSummary(ctx) } func (s *Service) Vehicles(ctx context.Context, query url.Values) (Page[VehicleRow], error) { return s.store.Vehicles(ctx, query) } func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string) (VehicleDetail, error) { vin = strings.TrimSpace(vin) protocol = strings.TrimSpace(protocol) vehicleQuery := url.Values{"keyword": {vin}, "limit": {"10"}} realtimeQuery := url.Values{"vin": {vin}, "limit": {"20"}} historyQuery := url.Values{"vin": {vin}, "limit": {"20"}} rawQuery := RawFrameQuery{VIN: vin, IncludeFields: true, Limit: 10} mileageQuery := url.Values{"vin": {vin}, "limit": {"20"}} if protocol != "" { realtimeQuery.Set("protocol", protocol) historyQuery.Set("protocol", protocol) rawQuery.Protocol = protocol } vehicles, err := s.store.Vehicles(ctx, vehicleQuery) if err != nil { return VehicleDetail{}, err } realtime, err := s.store.RealtimeLocations(ctx, realtimeQuery) if err != nil { return VehicleDetail{}, err } history, err := s.store.HistoryLocationsFromTDengine(ctx, historyQuery) if err != nil { return VehicleDetail{}, err } raw, err := s.RawFrames(ctx, rawQuery) if err != nil { return VehicleDetail{}, err } mileage, err := s.store.DailyMileage(ctx, mileageQuery) if err != nil { return VehicleDetail{}, err } var identity *VehicleRow for index := range vehicles.Items { if strings.EqualFold(vehicles.Items[index].VIN, vin) { identity = &vehicles.Items[index] break } } if identity == nil && len(vehicles.Items) > 0 { identity = &vehicles.Items[0] } return VehicleDetail{ VIN: vin, Identity: identity, Sources: vehicleSources(vehicles.Items, realtime.Items, raw.Items, mileage.Items), Realtime: realtime.Items, History: history, Raw: raw, Mileage: mileage, }, nil } func (s *Service) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) { return s.store.RealtimeLocations(ctx, query) } func (s *Service) HistoryLocations(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) { return s.store.HistoryLocationsFromTDengine(ctx, query) } func (s *Service) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawFrameRow], error) { if query.Limit <= 0 || query.Limit > 500 { query.Limit = 100 } return s.store.RawFrames(ctx, query) } func (s *Service) DailyMileage(ctx context.Context, query url.Values) (Page[DailyMileageRow], error) { return s.store.DailyMileage(ctx, query) } func (s *Service) QualityIssues(ctx context.Context, query url.Values) (Page[QualityIssueRow], error) { return s.store.QualityIssues(ctx, query) } func (s *Service) OpsHealth(ctx context.Context) (OpsHealth, error) { return s.store.OpsHealth(ctx) } func vehicleSources(vehicles []VehicleRow, realtime []RealtimeLocationRow, raw []RawFrameRow, mileage []DailyMileageRow) []string { seen := map[string]struct{}{} add := func(value string) { value = strings.TrimSpace(value) if value == "" { return } seen[value] = struct{}{} } for _, row := range vehicles { add(row.Protocol) } for _, row := range realtime { add(row.Protocol) } for _, row := range raw { add(row.Protocol) } for _, row := range mileage { add(row.Source) } out := make([]string, 0, len(seen)) for source := range seen { out = append(out, source) } sort.Strings(out) return out }