diff --git a/go/vehicle-gateway/internal/realtime/kv.go b/go/vehicle-gateway/internal/realtime/kv.go index 5325dcc1..78db21c9 100644 --- a/go/vehicle-gateway/internal/realtime/kv.go +++ b/go/vehicle-gateway/internal/realtime/kv.go @@ -30,6 +30,7 @@ func BuildFieldsEnvelope(env envelope.FrameEnvelope) (envelope.FrameEnvelope, bo if !ok { return envelope.FrameEnvelope{}, false } + statFields := fieldsWithCanonicalStats(fields, env.Fields) out := envelope.FrameEnvelope{ EventID: env.StableEventID() + ":fields", TraceID: env.TraceID, @@ -44,7 +45,7 @@ func BuildFieldsEnvelope(env envelope.FrameEnvelope) (envelope.FrameEnvelope, bo SourceEndpoint: env.SourceEndpoint, EventTimeMS: env.EventTimeMS, ReceivedAtMS: env.ReceivedAtMS, - Fields: fields, + Fields: statFields, ParsedFields: fields, ParsedFieldTypes: fieldTypes, Parsed: map[string]any{ @@ -56,6 +57,22 @@ func BuildFieldsEnvelope(env envelope.FrameEnvelope) (envelope.FrameEnvelope, bo return out, true } +func fieldsWithCanonicalStats(mapped map[string]any, canonical map[string]any) map[string]any { + out := cloneAnyMap(mapped) + for _, key := range []string{ + envelope.FieldTotalMileageKM, + envelope.FieldSpeedKMH, + envelope.FieldSOCPercent, + envelope.FieldLongitude, + envelope.FieldLatitude, + } { + if value, ok := canonical[key]; ok && value != nil { + out[key] = value + } + } + return out +} + func EnsureParsedFields(env *envelope.FrameEnvelope) bool { if env == nil { return false diff --git a/go/vehicle-gateway/internal/realtime/kv_test.go b/go/vehicle-gateway/internal/realtime/kv_test.go index d10b2b35..d61eee15 100644 --- a/go/vehicle-gateway/internal/realtime/kv_test.go +++ b/go/vehicle-gateway/internal/realtime/kv_test.go @@ -39,7 +39,7 @@ func TestRealtimeKVFieldsFromGB32960ParsedDomains(t *testing.T) { } } -func TestBuildFieldsEnvelopeUsesOnlyMappedFieldNames(t *testing.T) { +func TestBuildFieldsEnvelopeKeepsMappedAndCanonicalStatFields(t *testing.T) { fieldsEnv, ok := BuildFieldsEnvelope(envelope.FrameEnvelope{ Protocol: envelope.ProtocolGB32960, VIN: "VIN001", @@ -65,9 +65,9 @@ func TestBuildFieldsEnvelopeUsesOnlyMappedFieldNames(t *testing.T) { if !ok { t.Fatal("BuildFieldsEnvelope() should emit mapped fields") } - for _, bareKey := range []string{"charge_status", "soc_percent", "total_mileage_km"} { + for _, bareKey := range []string{"charge_status"} { if _, exists := fieldsEnv.Fields[bareKey]; exists { - t.Fatalf("fields envelope should not expose bare env.Fields key %q: %#v", bareKey, fieldsEnv.Fields) + t.Fatalf("fields envelope should not expose non-canonical bare env.Fields key %q: %#v", bareKey, fieldsEnv.Fields) } } for _, mappedKey := range []string{ @@ -79,6 +79,14 @@ func TestBuildFieldsEnvelopeUsesOnlyMappedFieldNames(t *testing.T) { t.Fatalf("fields envelope missing mapped key %q: %#v", mappedKey, fieldsEnv.Fields) } } + for _, canonicalKey := range []string{envelope.FieldSOCPercent, envelope.FieldTotalMileageKM} { + if _, exists := fieldsEnv.Fields[canonicalKey]; !exists { + t.Fatalf("fields envelope should keep canonical stat key %q: %#v", canonicalKey, fieldsEnv.Fields) + } + if _, exists := fieldsEnv.ParsedFields[canonicalKey]; exists { + t.Fatalf("parsed fields should stay protocol-mapped without canonical duplicate %q: %#v", canonicalKey, fieldsEnv.ParsedFields) + } + } } func TestRealtimeKVFieldsFromJT808ParsedFields(t *testing.T) {