feat(stats): expose selected mileage source
This commit is contained in:
@@ -27,14 +27,17 @@ type MetricQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MetricRow struct {
|
type MetricRow struct {
|
||||||
VIN string `json:"vin"`
|
VIN string `json:"vin"`
|
||||||
StatDate string `json:"stat_date"`
|
StatDate string `json:"stat_date"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
DailyMileageKM float64 `json:"daily_mileage_km"`
|
DailyMileageKM float64 `json:"daily_mileage_km"`
|
||||||
FirstTotalMileageKM *float64 `json:"first_total_mileage_km,omitempty"`
|
FirstTotalMileageKM *float64 `json:"first_total_mileage_km,omitempty"`
|
||||||
LatestTotalMileageKM *float64 `json:"latest_total_mileage_km,omitempty"`
|
LatestTotalMileageKM *float64 `json:"latest_total_mileage_km,omitempty"`
|
||||||
SampleCount int64 `json:"sample_count"`
|
TrustedSourceKey *string `json:"trusted_source_key,omitempty"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
TrustedPhone *string `json:"trusted_phone,omitempty"`
|
||||||
|
TrustedSourceEndpoint *string `json:"trusted_source_endpoint,omitempty"`
|
||||||
|
SampleCount int64 `json:"sample_count"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetricRepository struct {
|
type MetricRepository struct {
|
||||||
@@ -64,6 +67,9 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr
|
|||||||
var updatedAt scanDateTime
|
var updatedAt scanDateTime
|
||||||
var first sql.NullFloat64
|
var first sql.NullFloat64
|
||||||
var latest sql.NullFloat64
|
var latest sql.NullFloat64
|
||||||
|
var trustedSourceKey sql.NullString
|
||||||
|
var trustedPhone sql.NullString
|
||||||
|
var trustedSourceEndpoint sql.NullString
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&row.VIN,
|
&row.VIN,
|
||||||
&statDate,
|
&statDate,
|
||||||
@@ -71,6 +77,9 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr
|
|||||||
&row.DailyMileageKM,
|
&row.DailyMileageKM,
|
||||||
&first,
|
&first,
|
||||||
&latest,
|
&latest,
|
||||||
|
&trustedSourceKey,
|
||||||
|
&trustedPhone,
|
||||||
|
&trustedSourceEndpoint,
|
||||||
&row.SampleCount,
|
&row.SampleCount,
|
||||||
&updatedAt,
|
&updatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -84,6 +93,15 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr
|
|||||||
if latest.Valid {
|
if latest.Valid {
|
||||||
row.LatestTotalMileageKM = &latest.Float64
|
row.LatestTotalMileageKM = &latest.Float64
|
||||||
}
|
}
|
||||||
|
if trustedSourceKey.Valid {
|
||||||
|
row.TrustedSourceKey = &trustedSourceKey.String
|
||||||
|
}
|
||||||
|
if trustedPhone.Valid {
|
||||||
|
row.TrustedPhone = &trustedPhone.String
|
||||||
|
}
|
||||||
|
if trustedSourceEndpoint.Valid {
|
||||||
|
row.TrustedSourceEndpoint = &trustedSourceEndpoint.String
|
||||||
|
}
|
||||||
out = append(out, row)
|
out = append(out, row)
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
@@ -120,7 +138,10 @@ func normalizeMetricQuery(query MetricQuery) MetricQuery {
|
|||||||
|
|
||||||
func buildMetricSQL(query MetricQuery) (string, []any) {
|
func buildMetricSQL(query MetricQuery) (string, []any) {
|
||||||
where, args := buildMetricWhere(query)
|
where, args := buildMetricWhere(query)
|
||||||
sqlText := `SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage`
|
sqlText := `SELECT vin, stat_date, protocol, daily_mileage_km,
|
||||||
|
first_total_mileage_km, latest_total_mileage_km,
|
||||||
|
trusted_source_key, trusted_phone, trusted_source_endpoint,
|
||||||
|
sample_count, updated_at FROM vehicle_daily_mileage`
|
||||||
if len(where) > 0 {
|
if len(where) > 0 {
|
||||||
sqlText += " WHERE " + strings.Join(where, " AND ")
|
sqlText += " WHERE " + strings.Join(where, " AND ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,19 @@ func TestMetricRepositoryQueriesDailyMetricsWithFilters(t *testing.T) {
|
|||||||
t.Fatalf("sqlmock.New() error = %v", err)
|
t.Fatalf("sqlmock.New() error = %v", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage").
|
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage").
|
||||||
WithArgs("LKLG7C4E3NA774736", "JT808", "2026-07-01", "2026-07-01", 20, 0).
|
WithArgs("LKLG7C4E3NA774736", "JT808", "2026-07-01", "2026-07-01", 20, 0).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
"vin", "stat_date", "protocol", "daily_mileage_km",
|
"vin", "stat_date", "protocol", "daily_mileage_km",
|
||||||
"first_total_mileage_km", "latest_total_mileage_km", "sample_count",
|
"first_total_mileage_km", "latest_total_mileage_km",
|
||||||
|
"trusted_source_key", "trusted_phone", "trusted_source_endpoint",
|
||||||
|
"sample_count",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
}).AddRow(
|
}).AddRow(
|
||||||
"LKLG7C4E3NA774736", time.Date(2026, 7, 1, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)), "JT808", 12.3,
|
"LKLG7C4E3NA774736", time.Date(2026, 7, 1, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)), "JT808", 12.3,
|
||||||
12345.6, 12357.9, 13, time.Date(2026, 7, 1, 23, 9, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
|
12345.6, 12357.9,
|
||||||
|
nil, nil, nil,
|
||||||
|
13, time.Date(2026, 7, 1, 23, 9, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)),
|
||||||
))
|
))
|
||||||
|
|
||||||
repository := NewMetricRepository(db)
|
repository := NewMetricRepository(db)
|
||||||
@@ -54,6 +58,39 @@ func TestMetricRepositoryQueriesDailyMetricsWithFilters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMetricQueryReturnsSelectedSourceFields(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock.New() error = %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage").
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
|
"vin", "stat_date", "protocol", "daily_mileage_km",
|
||||||
|
"first_total_mileage_km", "latest_total_mileage_km",
|
||||||
|
"trusted_source_key", "trusted_phone", "trusted_source_endpoint",
|
||||||
|
"sample_count", "updated_at",
|
||||||
|
}).AddRow(
|
||||||
|
"LA9GG64L7PBAF4001", "2026-07-08", "JT808", 23.1,
|
||||||
|
4100.8, 4123.9,
|
||||||
|
"JT808:13307765812@115.231.168.135", "13307765812", "115.231.168.135:20215",
|
||||||
|
1, "2026-07-08 13:30:57",
|
||||||
|
))
|
||||||
|
|
||||||
|
repo := NewMetricRepository(db)
|
||||||
|
got, err := repo.Query(context.Background(), MetricQuery{
|
||||||
|
VIN: "LA9GG64L7PBAF4001",
|
||||||
|
Protocol: "JT808",
|
||||||
|
Limit: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Query() error = %v", err)
|
||||||
|
}
|
||||||
|
if got[0].TrustedSourceKey == nil || *got[0].TrustedSourceKey != "JT808:13307765812@115.231.168.135" {
|
||||||
|
t.Fatalf("trusted source = %#v", got[0].TrustedSourceKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMetricHandlerReturnsDailyMetrics(t *testing.T) {
|
func TestMetricHandlerReturnsDailyMetrics(t *testing.T) {
|
||||||
db, mock, err := sqlmock.New()
|
db, mock, err := sqlmock.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,15 +100,19 @@ func TestMetricHandlerReturnsDailyMetrics(t *testing.T) {
|
|||||||
mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM vehicle_daily_mileage").
|
mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM vehicle_daily_mileage").
|
||||||
WithArgs("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01").
|
WithArgs("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01").
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(42))
|
WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(42))
|
||||||
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage").
|
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage").
|
||||||
WithArgs("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01", 50, 0).
|
WithArgs("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01", 50, 0).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
"vin", "stat_date", "protocol", "daily_mileage_km",
|
"vin", "stat_date", "protocol", "daily_mileage_km",
|
||||||
"first_total_mileage_km", "latest_total_mileage_km", "sample_count",
|
"first_total_mileage_km", "latest_total_mileage_km",
|
||||||
|
"trusted_source_key", "trusted_phone", "trusted_source_endpoint",
|
||||||
|
"sample_count",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
}).AddRow(
|
}).AddRow(
|
||||||
"LB9A32A21R0LS1707", "2020-07-01", "GB32960", 0.0,
|
"LB9A32A21R0LS1707", "2020-07-01", "GB32960", 0.0,
|
||||||
53490.9, 53490.9, 3, "2026-07-01 22:28:25",
|
53490.9, 53490.9,
|
||||||
|
nil, nil, nil,
|
||||||
|
3, "2026-07-01 22:28:25",
|
||||||
))
|
))
|
||||||
|
|
||||||
handler := NewMetricHandler(NewMetricRepository(db))
|
handler := NewMetricHandler(NewMetricRepository(db))
|
||||||
@@ -103,11 +144,13 @@ func TestMetricHandlerReturnsEmptyItemsArrayWhenNoRows(t *testing.T) {
|
|||||||
t.Fatalf("sqlmock.New() error = %v", err)
|
t.Fatalf("sqlmock.New() error = %v", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage").
|
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage").
|
||||||
WithArgs("YUTONG_MQTT", 50, 0).
|
WithArgs("YUTONG_MQTT", 50, 0).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
"vin", "stat_date", "protocol", "daily_mileage_km",
|
"vin", "stat_date", "protocol", "daily_mileage_km",
|
||||||
"first_total_mileage_km", "latest_total_mileage_km", "sample_count",
|
"first_total_mileage_km", "latest_total_mileage_km",
|
||||||
|
"trusted_source_key", "trusted_phone", "trusted_source_endpoint",
|
||||||
|
"sample_count",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -135,15 +178,19 @@ func TestMetricHandlerSkipsTotalCountByDefault(t *testing.T) {
|
|||||||
t.Fatalf("sqlmock.New() error = %v", err)
|
t.Fatalf("sqlmock.New() error = %v", err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, sample_count, updated_at FROM vehicle_daily_mileage").
|
mock.ExpectQuery("SELECT vin, stat_date, protocol, daily_mileage_km, first_total_mileage_km, latest_total_mileage_km, trusted_source_key, trusted_phone, trusted_source_endpoint, sample_count, updated_at FROM vehicle_daily_mileage").
|
||||||
WithArgs("JT808", 1, 0).
|
WithArgs("JT808", 1, 0).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{
|
WillReturnRows(sqlmock.NewRows([]string{
|
||||||
"vin", "stat_date", "protocol", "daily_mileage_km",
|
"vin", "stat_date", "protocol", "daily_mileage_km",
|
||||||
"first_total_mileage_km", "latest_total_mileage_km", "sample_count",
|
"first_total_mileage_km", "latest_total_mileage_km",
|
||||||
|
"trusted_source_key", "trusted_phone", "trusted_source_endpoint",
|
||||||
|
"sample_count",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
}).AddRow(
|
}).AddRow(
|
||||||
"LKLG7C4E3NA774736", "2026-07-02", "JT808", 12.3,
|
"LKLG7C4E3NA774736", "2026-07-02", "JT808", 12.3,
|
||||||
8792.8, 8805.1, 30, "2026-07-02 23:59:59",
|
8792.8, 8805.1,
|
||||||
|
nil, nil, nil,
|
||||||
|
30, "2026-07-02 23:59:59",
|
||||||
))
|
))
|
||||||
|
|
||||||
handler := NewMetricHandler(NewMetricRepository(db))
|
handler := NewMetricHandler(NewMetricRepository(db))
|
||||||
|
|||||||
Reference in New Issue
Block a user