feat(platform): separate vehicle lookup key from VIN
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -56,7 +57,7 @@ func TestHandlerVehicleDetail(t *testing.T) {
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
for _, want := range []string{"identity", "realtimeSummary", "realtime", "history", "raw", "mileage", "quality", "sources", "sourceStatus"} {
|
||||
for _, want := range []string{"lookupKey", "lookupResolved", "identity", "realtimeSummary", "realtime", "history", "raw", "mileage", "quality", "sources", "sourceStatus"} {
|
||||
if !strings.Contains(rec.Body.String(), want) {
|
||||
t.Fatalf("response missing %q: %s", want, rec.Body.String())
|
||||
}
|
||||
@@ -89,6 +90,30 @@ func TestHandlerVehicleDetailAcceptsKeyword(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerVehicleDetailKeepsUnresolvedLookupOutOfVIN(t *testing.T) {
|
||||
handler := NewHandler(NewService(NewMockStore()))
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/vehicles/detail?keyword=64646848247", nil)
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
for _, want := range []string{`"lookupKey":"64646848247"`, `"lookupResolved":false`, `"vin":""`} {
|
||||
if !strings.Contains(rec.Body.String(), want) {
|
||||
t.Fatalf("response missing %q: %s", want, rec.Body.String())
|
||||
}
|
||||
}
|
||||
var body struct {
|
||||
Data VehicleDetail `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("response JSON should decode: %v body=%s", err, rec.Body.String())
|
||||
}
|
||||
if body.Data.VIN == "64646848247" {
|
||||
t.Fatalf("unresolved lookup should not be returned as top-level VIN: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRealtimeLocations(t *testing.T) {
|
||||
handler := NewHandler(NewService(NewMockStore()))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -56,6 +56,8 @@ type VehicleCoverageRow struct {
|
||||
|
||||
type VehicleDetail struct {
|
||||
VIN string `json:"vin"`
|
||||
LookupKey string `json:"lookupKey"`
|
||||
LookupResolved bool `json:"lookupResolved"`
|
||||
Identity *VehicleRow `json:"identity,omitempty"`
|
||||
RealtimeSummary *VehicleRealtimeRow `json:"realtimeSummary,omitempty"`
|
||||
Sources []string `json:"sources"`
|
||||
|
||||
@@ -67,23 +67,29 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
identity := resolveVehicleIdentity(keyword, vehicles.Items)
|
||||
resolvedVIN := keyword
|
||||
resolvedVIN := ""
|
||||
if identity != nil && strings.TrimSpace(identity.VIN) != "" {
|
||||
resolvedVIN = identity.VIN
|
||||
} else if isLikelyVIN(keyword) {
|
||||
resolvedVIN = keyword
|
||||
}
|
||||
queryVIN := resolvedVIN
|
||||
if queryVIN == "" {
|
||||
queryVIN = keyword
|
||||
}
|
||||
|
||||
realtimeQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
|
||||
historyQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
|
||||
rawQuery := RawFrameQuery{VIN: resolvedVIN, IncludeFields: true, Limit: 10}
|
||||
mileageQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
|
||||
qualityQuery := url.Values{"vin": {resolvedVIN}, "limit": {"20"}}
|
||||
realtimeQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
|
||||
historyQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
|
||||
rawQuery := RawFrameQuery{VIN: queryVIN, IncludeFields: true, Limit: 10}
|
||||
mileageQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
|
||||
qualityQuery := url.Values{"vin": {queryVIN}, "limit": {"20"}}
|
||||
if protocol != "" {
|
||||
realtimeQuery.Set("protocol", protocol)
|
||||
historyQuery.Set("protocol", protocol)
|
||||
rawQuery.Protocol = protocol
|
||||
qualityQuery.Set("protocol", protocol)
|
||||
}
|
||||
realtimeSummary, err := s.store.VehicleRealtime(ctx, url.Values{"vin": {resolvedVIN}, "limit": {"1"}})
|
||||
realtimeSummary, err := s.store.VehicleRealtime(ctx, url.Values{"vin": {queryVIN}, "limit": {"1"}})
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
@@ -112,9 +118,11 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
sourceStatus := vehicleSourceStatus(vehicles.Items, realtime.Items, history.Items, raw.Items, mileage.Items)
|
||||
sourceStatus = s.enrichVehicleSourceStatus(ctx, resolvedVIN, sourceStatus)
|
||||
sourceStatus = s.enrichVehicleSourceStatus(ctx, queryVIN, sourceStatus)
|
||||
return VehicleDetail{
|
||||
VIN: resolvedVIN,
|
||||
LookupKey: keyword,
|
||||
LookupResolved: resolvedVIN != "",
|
||||
Identity: identity,
|
||||
RealtimeSummary: summary,
|
||||
Sources: sourceNames(sourceStatus),
|
||||
@@ -172,6 +180,23 @@ func resolveVehicleIdentity(keyword string, vehicles []VehicleRow) *VehicleRow {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLikelyVIN(value string) bool {
|
||||
value = strings.ToUpper(strings.TrimSpace(value))
|
||||
if len(value) != 17 {
|
||||
return false
|
||||
}
|
||||
for _, char := range value {
|
||||
if char >= '0' && char <= '9' {
|
||||
continue
|
||||
}
|
||||
if char >= 'A' && char <= 'Z' && char != 'I' && char != 'O' && char != 'Q' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) RealtimeLocations(ctx context.Context, query url.Values) (Page[RealtimeLocationRow], error) {
|
||||
return s.store.RealtimeLocations(ctx, query)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user