From f4d820673673011a4e6fd250c0e0d680c9f6effc Mon Sep 17 00:00:00 2001 From: James Reategui Date: Mon, 28 Jul 2025 20:32:01 -0400 Subject: [PATCH 1/6] implement eleva api --- internal/config/settings.go | 3 + internal/core/models/vin_decoding_models.go | 24 ++++ .../infrastructure/gateways/kaufmann_api.go | 132 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 internal/infrastructure/gateways/kaufmann_api.go diff --git a/internal/config/settings.go b/internal/config/settings.go index 16f5af54..5a2baed0 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -55,6 +55,9 @@ type Settings struct { Japan17VINPassword string `yaml:"JAPAN17_VIN_PASSWORD"` CarVxUserID string `yaml:"CAR_VX_USER_ID"` CarVxAPIKey string `yaml:"CAR_VX_API_KEY"` + // used for Kaufmann API + ElevaUsername string `yaml:"ELEVA_USERNAME"` + ElevaPassword string `yaml:"ELEVA_PASSWORD"` } func (s *Settings) IsProd() bool { diff --git a/internal/core/models/vin_decoding_models.go b/internal/core/models/vin_decoding_models.go index 41e67ba2..ed5a8f80 100644 --- a/internal/core/models/vin_decoding_models.go +++ b/internal/core/models/vin_decoding_models.go @@ -444,3 +444,27 @@ type CarVxResponse struct { } `json:"data"` Error string `json:"error"` } + +type ElevaVINResponse struct { + Error int `json:"error"` + Message string `json:"message"` + Data struct { + Client struct { + Rut string `json:"rut"` + ClientId int `json:"clientId"` + Name string `json:"name"` + Lastname string `json:"lastname"` + Email string `json:"email"` + Phone string `json:"phone"` + BusinessName string `json:"businessName"` + } `json:"client"` + Vehicle struct { + Plate string `json:"plate"` + Chassis string `json:"chassis"` + Model string `json:"model"` + Kms int `json:"kms"` + Baumuster string `json:"baumuster"` + Brand string `json:"brand"` + } `json:"vehicle"` + } `json:"data"` +} diff --git a/internal/infrastructure/gateways/kaufmann_api.go b/internal/infrastructure/gateways/kaufmann_api.go new file mode 100644 index 00000000..697800de --- /dev/null +++ b/internal/infrastructure/gateways/kaufmann_api.go @@ -0,0 +1,132 @@ +package gateways + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/DIMO-Network/device-definitions-api/internal/config" + coremodels "github.com/DIMO-Network/device-definitions-api/internal/core/models" + "io" + "net/http" + "time" +) + +type ElevaConfig struct { + Username string + Password string +} + +// ElevaAPI is used to call Kaufmann for VIN decoding in Chile +type ElevaAPI struct { + client *http.Client + config ElevaConfig + accessToken string + tokenExpires time.Time +} + +func NewElevaAPI(settings config.Settings) *ElevaAPI { + return &ElevaAPI{ + client: &http.Client{ + Timeout: 10 * time.Second, + }, + config: ElevaConfig{ + Username: settings.ElevaUsername, + Password: settings.ElevaPassword, + }, + } +} + +// how long does the access token last +const tokenExpiration = 15 * time.Minute // assumed + +func (e *ElevaAPI) getAccessToken() error { + if e.accessToken != "" && time.Now().Before(e.tokenExpires) { + return nil + } + + loginURL := "https://api-prd-js.eleva-services.com/v4/auth/login" + + payload := map[string]string{ + "username": e.config.Username, + "password": e.config.Password, + } + body, _ := json.Marshal(payload) + + req, err := http.NewRequest("POST", loginURL, bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("creating login request: %w", err) + } + req.Header.Set("accept", "application/json") + req.Header.Set("content-type", "application/json") + req.Header.Set("origin", "https://portalprepago.elevapp.io") + req.Header.Set("referer", "https://portalprepago.elevapp.io/") + + resp, err := e.client.Do(req) + if err != nil { + return fmt.Errorf("performing login request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("auth failed with status %d", resp.StatusCode) + } + + var result struct { + Data struct { + AccessToken string `json:"accessToken"` + } `json:"data"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("decoding auth response: %w", err) + } + + if result.Data.AccessToken == "" { + return fmt.Errorf("no access token returned") + } + + e.accessToken = result.Data.AccessToken + e.tokenExpires = time.Now().Add(tokenExpiration) + return nil +} + +func (e *ElevaAPI) GetVINInfo(plateOrVIN string) (*coremodels.ElevaVINResponse, error) { + if err := e.getAccessToken(); err != nil { + return nil, err + } + + url := "https://api-prd-js.eleva-services.com/v4/showcase-vehicle/client-summary" + payload := map[string]string{ + "plateOrChassis": plateOrVIN, + } + body, _ := json.Marshal(payload) + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return nil, fmt.Errorf("creating vin info request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+e.accessToken) + + resp, err := e.client.Do(req) + if err != nil { + return nil, fmt.Errorf("performing vin info request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("vin info failed (%d): %s", resp.StatusCode, string(bodyBytes)) + } + + jsonResp, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed reading vin info response: %w", err) + } + v := &coremodels.ElevaVINResponse{} + err = json.Unmarshal(jsonResp, &v) + if err != nil { + return nil, fmt.Errorf("failed decoding vin info response: %w", err) + } + + return v, nil +} From 2ac63638d464752e5fb8489d22b4a55126fa9e47 Mon Sep 17 00:00:00 2001 From: James Reategui Date: Mon, 28 Jul 2025 21:22:27 -0400 Subject: [PATCH 2/6] add eleva api to vin decoding svc, update test --- internal/core/models/vin_decoding_models.go | 17 +++--- .../core/services/vin_decoding_service.go | 38 ++++++++++++- .../services/vin_decoding_service_test.go | 4 +- .../infrastructure/gateways/kaufmann_api.go | 16 ++++-- .../gateways/mocks/kaufmann_api_mock.go | 56 +++++++++++++++++++ 5 files changed, 114 insertions(+), 17 deletions(-) create mode 100644 internal/infrastructure/gateways/mocks/kaufmann_api_mock.go diff --git a/internal/core/models/vin_decoding_models.go b/internal/core/models/vin_decoding_models.go index ed5a8f80..ab3ea027 100644 --- a/internal/core/models/vin_decoding_models.go +++ b/internal/core/models/vin_decoding_models.go @@ -12,14 +12,15 @@ import ( type DecodeProviderEnum string const ( - DrivlyProvider DecodeProviderEnum = "drivly" - VincarioProvider DecodeProviderEnum = "vincario" - AutoIsoProvider DecodeProviderEnum = "autoiso" - DATGroupProvider DecodeProviderEnum = "dat" - AllProviders DecodeProviderEnum = "" - TeslaProvider DecodeProviderEnum = "tesla" - Japan17VIN DecodeProviderEnum = "japan17vin" - CarVXVIN DecodeProviderEnum = "carvxvin" + DrivlyProvider DecodeProviderEnum = "drivly" + VincarioProvider DecodeProviderEnum = "vincario" + AutoIsoProvider DecodeProviderEnum = "autoiso" + DATGroupProvider DecodeProviderEnum = "dat" + AllProviders DecodeProviderEnum = "" + TeslaProvider DecodeProviderEnum = "tesla" + Japan17VIN DecodeProviderEnum = "japan17vin" + CarVXVIN DecodeProviderEnum = "carvxvin" + ElevaKaufmannProvider DecodeProviderEnum = "eleva" ) type VINDecodingInfoData struct { diff --git a/internal/core/services/vin_decoding_service.go b/internal/core/services/vin_decoding_service.go index 6cd9e4e3..07709d42 100644 --- a/internal/core/services/vin_decoding_service.go +++ b/internal/core/services/vin_decoding_service.go @@ -37,15 +37,17 @@ type vinDecodingService struct { DATGroupAPIService gateways.DATGroupAPIService japan17VINAPI gateways.Japan17VINAPI carvxAPI gateways.CarVxVINAPI + elevaAPI gateways.ElevaAPI onChainSvc gateways.DeviceDefinitionOnChainService dbs func() *db.ReaderWriter } func NewVINDecodingService(drivlyAPISvc gateways.DrivlyAPIService, vincarioAPISvc gateways.VincarioAPIService, autoIso gateways.AutoIsoAPIService, logger *zerolog.Logger, onChainSvc gateways.DeviceDefinitionOnChainService, datGroupAPIService gateways.DATGroupAPIService, dbs func() *db.ReaderWriter, - japan17VINAPI gateways.Japan17VINAPI, carvxAPI gateways.CarVxVINAPI) VINDecodingService { + japan17VINAPI gateways.Japan17VINAPI, carvxAPI gateways.CarVxVINAPI, elevaAPI gateways.ElevaAPI) VINDecodingService { return &vinDecodingService{drivlyAPISvc: drivlyAPISvc, vincarioAPISvc: vincarioAPISvc, autoIsoAPIService: autoIso, - japan17VINAPI: japan17VINAPI, carvxAPI: carvxAPI, logger: logger, onChainSvc: onChainSvc, DATGroupAPIService: datGroupAPIService, dbs: dbs} + japan17VINAPI: japan17VINAPI, carvxAPI: carvxAPI, logger: logger, onChainSvc: onChainSvc, + DATGroupAPIService: datGroupAPIService, dbs: dbs, elevaAPI: elevaAPI} } func (c vinDecodingService) GetVIN(ctx context.Context, vin string, provider coremodels.DecodeProviderEnum, country string) (*coremodels.VINDecodingInfoData, *coremodels.VINDecodingVendorExtra, error) { @@ -56,7 +58,10 @@ func (c vinDecodingService) GetVIN(ctx context.Context, vin string, provider cor vin = strings.ToUpper(strings.TrimSpace(vin)) providersToTry := make([]coremodels.DecodeProviderEnum, 0) // check for japan chasis if all providers - if provider == coremodels.AllProviders && ((len(vin) < 17 && len(vin) > 10) || country == "JPN") { + if country == "CHL" { + providersToTry = append(providersToTry, coremodels.ElevaKaufmannProvider) + providersToTry = append(providersToTry, coremodels.VincarioProvider) // sometimes works in latam as backup + } else if provider == coremodels.AllProviders && ((len(vin) < 17 && len(vin) > 10) || country == "JPN") { providersToTry = append(providersToTry, coremodels.CarVXVIN) providersToTry = append(providersToTry, coremodels.Japan17VIN) } else if !ValidateVIN(vin) { @@ -227,6 +232,33 @@ func (c vinDecodingService) GetVIN(ctx context.Context, vin string, provider cor continue } return result, resultVendorExtra, nil + case coremodels.ElevaKaufmannProvider: + resultVendorExtra.VendorsTried = append(resultVendorExtra.VendorsTried, string(coremodels.ElevaKaufmannProvider)) + localLog.Info().Msgf("trying to decode VIN: %s with eleva", vin) + + vinInfo, err := c.elevaAPI.GetVINInfo(vin) + if err != nil { + errFinal = errors.Wrapf(err, "unable to decode vin: %s with eleva", vin) + continue + } + vinObj := vinutil.VIN(vin) + yr := vinObj.Year() + if yr < 2018 { + yr = time.Now().Year() - 1 + localLog.Info().Msgf("vin year is less than 2019 for eleva kaufmann, using %d instead", yr) + } + + result = &coremodels.VINDecodingInfoData{ + VIN: vin, + Make: vinInfo.Data.Vehicle.Brand, + Model: vinInfo.Data.Vehicle.Model, + SubModel: "", + Year: int32(yr), + StyleName: "", + Source: "ElevaKaufmann", + Raw: nil, + } + return result, resultVendorExtra, nil case coremodels.AllProviders: // this should never hit errFinal = fmt.Errorf("all providers - invalid option reached") diff --git a/internal/core/services/vin_decoding_service_test.go b/internal/core/services/vin_decoding_service_test.go index 4d01dee1..d31eab3d 100644 --- a/internal/core/services/vin_decoding_service_test.go +++ b/internal/core/services/vin_decoding_service_test.go @@ -37,6 +37,7 @@ type VINDecodingServiceSuite struct { mockOnChainSvc *mock_gateways.MockDeviceDefinitionOnChainService vinDecodingService VINDecodingService + mockElevaAPI *mock_gateways.MockElevaAPI } func TestVINDecodingService(t *testing.T) { @@ -62,10 +63,11 @@ func (s *VINDecodingServiceSuite) SetupTest() { s.mockDATGroupAPIService = mock_gateways.NewMockDATGroupAPIService(s.ctrl) s.mockJapan17VINAPI = mock_gateways.NewMockJapan17VINAPI(s.ctrl) s.mockCarvxAPI = mock_gateways.NewMockCarVxVINAPI(s.ctrl) + s.mockElevaAPI = mock_gateways.NewMockElevaAPI(s.ctrl) s.mockOnChainSvc = mock_gateways.NewMockDeviceDefinitionOnChainService(s.ctrl) s.vinDecodingService = NewVINDecodingService(s.mockDrivlyAPISvc, s.mockVincarioAPISvc, s.mockAutoIsoAPISvc, dbtesthelper.Logger(), - s.mockOnChainSvc, s.mockDATGroupAPIService, s.pdb.DBS, s.mockJapan17VINAPI, s.mockCarvxAPI) + s.mockOnChainSvc, s.mockDATGroupAPIService, s.pdb.DBS, s.mockJapan17VINAPI, s.mockCarvxAPI, s.mockElevaAPI) } func (s *VINDecodingServiceSuite) TearDownTest() { diff --git a/internal/infrastructure/gateways/kaufmann_api.go b/internal/infrastructure/gateways/kaufmann_api.go index 697800de..7c70d1d1 100644 --- a/internal/infrastructure/gateways/kaufmann_api.go +++ b/internal/infrastructure/gateways/kaufmann_api.go @@ -11,21 +11,27 @@ import ( "time" ) +//go:generate mockgen -source kaufmann_api.go -destination mocks/kaufmann_api_mock.go -package mocks + type ElevaConfig struct { Username string Password string } +type ElevaAPI interface { + GetVINInfo(vin string) (*coremodels.ElevaVINResponse, error) +} + // ElevaAPI is used to call Kaufmann for VIN decoding in Chile -type ElevaAPI struct { +type elevaAPI struct { client *http.Client config ElevaConfig accessToken string tokenExpires time.Time } -func NewElevaAPI(settings config.Settings) *ElevaAPI { - return &ElevaAPI{ +func NewElevaAPI(settings config.Settings) ElevaAPI { + return &elevaAPI{ client: &http.Client{ Timeout: 10 * time.Second, }, @@ -39,7 +45,7 @@ func NewElevaAPI(settings config.Settings) *ElevaAPI { // how long does the access token last const tokenExpiration = 15 * time.Minute // assumed -func (e *ElevaAPI) getAccessToken() error { +func (e *elevaAPI) getAccessToken() error { if e.accessToken != "" && time.Now().Before(e.tokenExpires) { return nil } @@ -89,7 +95,7 @@ func (e *ElevaAPI) getAccessToken() error { return nil } -func (e *ElevaAPI) GetVINInfo(plateOrVIN string) (*coremodels.ElevaVINResponse, error) { +func (e *elevaAPI) GetVINInfo(plateOrVIN string) (*coremodels.ElevaVINResponse, error) { if err := e.getAccessToken(); err != nil { return nil, err } diff --git a/internal/infrastructure/gateways/mocks/kaufmann_api_mock.go b/internal/infrastructure/gateways/mocks/kaufmann_api_mock.go new file mode 100644 index 00000000..c635c0bd --- /dev/null +++ b/internal/infrastructure/gateways/mocks/kaufmann_api_mock.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: kaufmann_api.go +// +// Generated by this command: +// +// mockgen -source kaufmann_api.go -destination mocks/kaufmann_api_mock.go -package mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + models "github.com/DIMO-Network/device-definitions-api/internal/core/models" + gomock "go.uber.org/mock/gomock" +) + +// MockElevaAPI is a mock of ElevaAPI interface. +type MockElevaAPI struct { + ctrl *gomock.Controller + recorder *MockElevaAPIMockRecorder + isgomock struct{} +} + +// MockElevaAPIMockRecorder is the mock recorder for MockElevaAPI. +type MockElevaAPIMockRecorder struct { + mock *MockElevaAPI +} + +// NewMockElevaAPI creates a new mock instance. +func NewMockElevaAPI(ctrl *gomock.Controller) *MockElevaAPI { + mock := &MockElevaAPI{ctrl: ctrl} + mock.recorder = &MockElevaAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockElevaAPI) EXPECT() *MockElevaAPIMockRecorder { + return m.recorder +} + +// GetVINInfo mocks base method. +func (m *MockElevaAPI) GetVINInfo(vin string) (*models.ElevaVINResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVINInfo", vin) + ret0, _ := ret[0].(*models.ElevaVINResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVINInfo indicates an expected call of GetVINInfo. +func (mr *MockElevaAPIMockRecorder) GetVINInfo(vin any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVINInfo", reflect.TypeOf((*MockElevaAPI)(nil).GetVINInfo), vin) +} From a2ddfb84d946634edfc769e5264378840a4ad1c1 Mon Sep 17 00:00:00 2001 From: James Reategui Date: Tue, 29 Jul 2025 14:54:11 -0400 Subject: [PATCH 3/6] secrets, linting, add test, fix --- .../templates/secret.yaml | 6 +++++ cmd/device-definitions-api/decode_vin.go | 3 ++- internal/api/api.go | 3 ++- internal/core/models/vin_decoding_models.go | 1 + internal/core/services/eleva_resp.json | 23 +++++++++++++++++ .../services/vin_decoding_service_test.go | 25 +++++++++++++++++++ .../infrastructure/gateways/kaufmann_api.go | 7 +++--- 7 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 internal/core/services/eleva_resp.json diff --git a/charts/device-definitions-api/templates/secret.yaml b/charts/device-definitions-api/templates/secret.yaml index 56c1a3ef..13847314 100644 --- a/charts/device-definitions-api/templates/secret.yaml +++ b/charts/device-definitions-api/templates/secret.yaml @@ -95,6 +95,12 @@ spec: - remoteRef: key: {{ .Release.Namespace }}/definitions/carvx/apikey secretKey: CAR_VX_API_KEY + - remoteRef: + key: {{ .Release.Namespace }}/definitions/kaufmann/username + secretKey: ELEVA_USERNAME + - remoteRef: + key: {{ .Release.Namespace }}/definitions/kaufmann/password + secretKey: ELEVA_PASSWORD secretStoreRef: kind: ClusterSecretStore name: aws-secretsmanager-secret-store diff --git a/cmd/device-definitions-api/decode_vin.go b/cmd/device-definitions-api/decode_vin.go index 6ab73832..598d8fbe 100644 --- a/cmd/device-definitions-api/decode_vin.go +++ b/cmd/device-definitions-api/decode_vin.go @@ -250,6 +250,7 @@ func instantiateVINDecodingSvc(ctx context.Context, settings *config.Settings, l vincarioAPI := gateways.NewVincarioAPIService(settings, logger) jp17vinAPI := gateways.NewJapan17VINAPI(logger, settings) carvxAPI := gateways.NewCarVxVINAPI(logger, settings) + elevaAPI := gateways.NewElevaAPI(settings) send, err := createSender(ctx, settings, logger) if err != nil { @@ -267,7 +268,7 @@ func instantiateVINDecodingSvc(ctx context.Context, settings *config.Settings, l } deviceDefinitionOnChainService := gateways.NewDeviceDefinitionOnChainService(settings, logger, ethClient, chainID, send, pdb.DBS) - vinDecodingService := services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, deviceDefinitionOnChainService, datAPI, pdb.DBS, jp17vinAPI, carvxAPI) + vinDecodingService := services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, deviceDefinitionOnChainService, datAPI, pdb.DBS, jp17vinAPI, carvxAPI, elevaAPI) return vinDecodingService } diff --git a/internal/api/api.go b/internal/api/api.go index 24146591..93c65b8d 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -62,6 +62,7 @@ func Run(ctx context.Context, logger zerolog.Logger, settings *config.Settings, autoIsoAPIService := gateways.NewAutoIsoAPIService(settings.AutoIsoAPIUid, settings.AutoIsoAPIKey) japan17VINAPI := gateways.NewJapan17VINAPI(&logger, settings) carvxAPI := gateways.NewCarVxVINAPI(&logger, settings) + elevaAPI := gateways.NewElevaAPI(settings) registryInstance, err := contracts.NewRegistry(settings.EthereumRegistryAddress, ethClient) if err != nil { logger.Fatal().Err(err).Msg("Failed to create registry query instance.") @@ -75,7 +76,7 @@ func Run(ctx context.Context, logger zerolog.Logger, settings *config.Settings, vinRepository := repositories.NewVINRepository(pdb.DBS, registryInstance, identityAPI) //cache services - vincDecodingService := services.NewVINDecodingService(drivlyAPIService, vincarioAPIService, autoIsoAPIService, &logger, ddOnChainService, datGroupWSService, pdb.DBS, japan17VINAPI, carvxAPI) + vincDecodingService := services.NewVINDecodingService(drivlyAPIService, vincarioAPIService, autoIsoAPIService, &logger, ddOnChainService, datGroupWSService, pdb.DBS, japan17VINAPI, carvxAPI, elevaAPI) powerTrainTypeService, err := services.NewPowerTrainTypeService("powertrain_type_rule.yaml", &logger, ddOnChainService) searchService := search.NewTypesenseAPIService(settings, &logger) if err != nil { diff --git a/internal/core/models/vin_decoding_models.go b/internal/core/models/vin_decoding_models.go index ab3ea027..e0e03b82 100644 --- a/internal/core/models/vin_decoding_models.go +++ b/internal/core/models/vin_decoding_models.go @@ -446,6 +446,7 @@ type CarVxResponse struct { Error string `json:"error"` } +// nolint type ElevaVINResponse struct { Error int `json:"error"` Message string `json:"message"` diff --git a/internal/core/services/eleva_resp.json b/internal/core/services/eleva_resp.json new file mode 100644 index 00000000..7983c78e --- /dev/null +++ b/internal/core/services/eleva_resp.json @@ -0,0 +1,23 @@ +{ + "error": 0, + "message": "Success", + "data": { + "client": { + "rut": "12866851-9", + "clientId": 13, + "name": "MACARENA", + "lastname": "RAMOS ALERCE", + "email": "maxhoja@gmail.com", + "phone": "995339163", + "businessName": "MACARENA RAMOS ALERCE" + }, + "vehicle": { + "plate": "SC-SS29", + "chassis": "W1K3F4GB9NN286196", + "model": "A 250", + "kms": 43879, + "baumuster": "177046", + "brand": "MERCEDES-BENZ" + } + } +} \ No newline at end of file diff --git a/internal/core/services/vin_decoding_service_test.go b/internal/core/services/vin_decoding_service_test.go index d31eab3d..543be6ec 100644 --- a/internal/core/services/vin_decoding_service_test.go +++ b/internal/core/services/vin_decoding_service_test.go @@ -110,6 +110,31 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_Japan17VIN_Success() { assert.Equal(s.T(), result.Year, int32(2022)) } +//go:embed eleva_resp.json +var elevaAPIResponse []byte + +func (s *VINDecodingServiceSuite) Test_VINDecodingService_KaufmannEleva_Success() { + ctx := context.Background() + const vin = "W1K3F4GB9NN286196" + const country = "CHL" // chile only + + vinInfoResp := &coremodels.ElevaVINResponse{} + err := json.Unmarshal(elevaAPIResponse, vinInfoResp) + require.NoError(s.T(), err) + s.mockElevaAPI.EXPECT().GetVINInfo(vin).Times(1).Return(vinInfoResp, nil) + + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + + result, _, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.AllProviders, country) + + s.NoError(err) + assert.Equal(s.T(), result.VIN, vin) + assert.Equal(s.T(), result.Source, coremodels.ElevaKaufmannProvider) + assert.Equal(s.T(), result.Make, "Mercedes-Benz") + assert.Equal(s.T(), result.Model, "A 250") + assert.Equal(s.T(), result.Year, int32(2022)) +} + func (s *VINDecodingServiceSuite) Test_VINDecodingService_Drivly_Success() { ctx := context.Background() const vin = "1FMCU0G61MUA52727" // ford escape 2021 diff --git a/internal/infrastructure/gateways/kaufmann_api.go b/internal/infrastructure/gateways/kaufmann_api.go index 7c70d1d1..285239f9 100644 --- a/internal/infrastructure/gateways/kaufmann_api.go +++ b/internal/infrastructure/gateways/kaufmann_api.go @@ -4,11 +4,12 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/DIMO-Network/device-definitions-api/internal/config" - coremodels "github.com/DIMO-Network/device-definitions-api/internal/core/models" "io" "net/http" "time" + + "github.com/DIMO-Network/device-definitions-api/internal/config" + coremodels "github.com/DIMO-Network/device-definitions-api/internal/core/models" ) //go:generate mockgen -source kaufmann_api.go -destination mocks/kaufmann_api_mock.go -package mocks @@ -30,7 +31,7 @@ type elevaAPI struct { tokenExpires time.Time } -func NewElevaAPI(settings config.Settings) ElevaAPI { +func NewElevaAPI(settings *config.Settings) ElevaAPI { return &elevaAPI{ client: &http.Client{ Timeout: 10 * time.Second, From 69a55fd74f02d29bae54e4e2e431701bca920600 Mon Sep 17 00:00:00 2001 From: James Reategui Date: Tue, 29 Jul 2025 15:15:46 -0400 Subject: [PATCH 4/6] fix test, make casing match, add console for eleva api --- cmd/device-definitions-api/decode_vin.go | 10 +++++++++ .../core/services/vin_decoding_service.go | 22 +++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/cmd/device-definitions-api/decode_vin.go b/cmd/device-definitions-api/decode_vin.go index 598d8fbe..7c812cfb 100644 --- a/cmd/device-definitions-api/decode_vin.go +++ b/cmd/device-definitions-api/decode_vin.go @@ -37,6 +37,7 @@ type decodeVINCmd struct { fromFile bool persistToDB bool carvx bool + eleva bool } func (*decodeVINCmd) Name() string { return "decodevin" } @@ -53,6 +54,7 @@ func (p *decodeVINCmd) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.vincario, "vincario", false, "use vincario vin decoder") f.BoolVar(&p.japan17vin, "japan17vin", false, "use japan17vin vin decoder") f.BoolVar(&p.carvx, "carvx", false, "use carvx vin decoder") + f.BoolVar(&p.eleva, "eleva", false, "use eleva kaufmann vin decoder") f.BoolVar(&p.fromFile, "from-file", false, "read vin from file in /tmp directory") f.BoolVar(&p.persistToDB, "persist-to-db", false, "persist successful vin decodings to db, table vin_numbers") } @@ -160,6 +162,14 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf fmt.Println("VIN Info:") fmt.Println(string(jsonBytes)) } + if p.eleva { + vinInfo, _, err = vinDecodingService.GetVIN(ctx, vin, coremodels.ElevaKaufmannProvider, country) + if err != nil { + fmt.Println(err.Error()) + continue + } + fmt.Printf("VIN Response: \n %+v\n", vinInfo) + } fmt.Println() if p.persistToDB { if vinInfo == nil || vinInfo.Model == "" { diff --git a/internal/core/services/vin_decoding_service.go b/internal/core/services/vin_decoding_service.go index 07709d42..e736d1f5 100644 --- a/internal/core/services/vin_decoding_service.go +++ b/internal/core/services/vin_decoding_service.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/DIMO-Network/shared/pkg/logfields" @@ -250,12 +251,12 @@ func (c vinDecodingService) GetVIN(ctx context.Context, vin string, provider cor result = &coremodels.VINDecodingInfoData{ VIN: vin, - Make: vinInfo.Data.Vehicle.Brand, + Make: titleCase(vinInfo.Data.Vehicle.Brand), // MERCEDES-BENZ to Mercedes-Benz Model: vinInfo.Data.Vehicle.Model, SubModel: "", Year: int32(yr), StyleName: "", - Source: "ElevaKaufmann", + Source: coremodels.ElevaKaufmannProvider, Raw: nil, } return result, resultVendorExtra, nil @@ -427,3 +428,20 @@ func validateVinDecoding(vdi *coremodels.VINDecodingInfoData) error { return nil } + +func titleCase(s string) string { + parts := strings.Split(s, "-") + + for i, part := range parts { + if len(part) == 0 { + continue + } + part = strings.ToLower(part) + + runes := []rune(part) + runes[0] = unicode.ToUpper(runes[0]) + + parts[i] = string(runes) + } + return strings.Join(parts, "-") +} From b399433bf41d8ced5d8760fdab453f81c03cdcf8 Mon Sep 17 00:00:00 2001 From: James Reategui Date: Tue, 29 Jul 2025 22:16:53 -0400 Subject: [PATCH 5/6] kaufmann fixes from manual validation --- cmd/device-definitions-api/decode_vin.go | 35 ++++++++++--------- .../infrastructure/gateways/kaufmann_api.go | 7 ++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/cmd/device-definitions-api/decode_vin.go b/cmd/device-definitions-api/decode_vin.go index 7c812cfb..4426ad93 100644 --- a/cmd/device-definitions-api/decode_vin.go +++ b/cmd/device-definitions-api/decode_vin.go @@ -45,7 +45,7 @@ func (*decodeVINCmd) Synopsis() string { return "tries decoding a vin with chosen provider - does not insert in our db" } func (*decodeVINCmd) Usage() string { - return `decodevin [-dat|-drivly|-vincario|-japan17vin|carvx|-from-file] ` + return `decodevin [-dat|-drivly|-vincario|-japan17vin|-carvx|-eleva|-from-file] ` } func (p *decodeVINCmd) SetFlags(f *flag.FlagSet) { @@ -261,24 +261,25 @@ func instantiateVINDecodingSvc(ctx context.Context, settings *config.Settings, l jp17vinAPI := gateways.NewJapan17VINAPI(logger, settings) carvxAPI := gateways.NewCarVxVINAPI(logger, settings) elevaAPI := gateways.NewElevaAPI(settings) + if settings.Environment == "local" { + return services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, nil, datAPI, pdb.DBS, jp17vinAPI, carvxAPI, elevaAPI) + } else { + send, err := createSender(ctx, settings, logger) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to create sender.") + } - send, err := createSender(ctx, settings, logger) - if err != nil { - logger.Fatal().Err(err).Msg("Failed to create sender.") - } + ethClient, err := ethclient.Dial(settings.EthereumRPCURL.String()) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to create Ethereum client.") + } - ethClient, err := ethclient.Dial(settings.EthereumRPCURL.String()) - if err != nil { - logger.Fatal().Err(err).Msg("Failed to create Ethereum client.") - } + chainID, err := ethClient.ChainID(ctx) + if err != nil { + logger.Fatal().Err(err).Msg("Couldn't retrieve chain id.") + } + deviceDefinitionOnChainService := gateways.NewDeviceDefinitionOnChainService(settings, logger, ethClient, chainID, send, pdb.DBS) - chainID, err := ethClient.ChainID(ctx) - if err != nil { - logger.Fatal().Err(err).Msg("Couldn't retrieve chain id.") + return services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, deviceDefinitionOnChainService, datAPI, pdb.DBS, jp17vinAPI, carvxAPI, elevaAPI) } - deviceDefinitionOnChainService := gateways.NewDeviceDefinitionOnChainService(settings, logger, ethClient, chainID, send, pdb.DBS) - - vinDecodingService := services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, deviceDefinitionOnChainService, datAPI, pdb.DBS, jp17vinAPI, carvxAPI, elevaAPI) - - return vinDecodingService } diff --git a/internal/infrastructure/gateways/kaufmann_api.go b/internal/infrastructure/gateways/kaufmann_api.go index 285239f9..70b572df 100644 --- a/internal/infrastructure/gateways/kaufmann_api.go +++ b/internal/infrastructure/gateways/kaufmann_api.go @@ -74,7 +74,7 @@ func (e *elevaAPI) getAccessToken() error { } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode > 299 { return fmt.Errorf("auth failed with status %d", resp.StatusCode) } @@ -120,7 +120,7 @@ func (e *elevaAPI) GetVINInfo(plateOrVIN string) (*coremodels.ElevaVINResponse, } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode > 299 { bodyBytes, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("vin info failed (%d): %s", resp.StatusCode, string(bodyBytes)) } @@ -134,6 +134,9 @@ func (e *elevaAPI) GetVINInfo(plateOrVIN string) (*coremodels.ElevaVINResponse, if err != nil { return nil, fmt.Errorf("failed decoding vin info response: %w", err) } + if v.Error > 0 { + return nil, fmt.Errorf("eleva api returned error: %s", v.Message) + } return v, nil } From ad4d3226244877600112ba622d8a81c976f2c580 Mon Sep 17 00:00:00 2001 From: James Reategui Date: Tue, 29 Jul 2025 22:18:13 -0400 Subject: [PATCH 6/6] lint fix --- cmd/device-definitions-api/decode_vin.go | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/device-definitions-api/decode_vin.go b/cmd/device-definitions-api/decode_vin.go index 4426ad93..dd4057ba 100644 --- a/cmd/device-definitions-api/decode_vin.go +++ b/cmd/device-definitions-api/decode_vin.go @@ -263,23 +263,23 @@ func instantiateVINDecodingSvc(ctx context.Context, settings *config.Settings, l elevaAPI := gateways.NewElevaAPI(settings) if settings.Environment == "local" { return services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, nil, datAPI, pdb.DBS, jp17vinAPI, carvxAPI, elevaAPI) - } else { - send, err := createSender(ctx, settings, logger) - if err != nil { - logger.Fatal().Err(err).Msg("Failed to create sender.") - } + } - ethClient, err := ethclient.Dial(settings.EthereumRPCURL.String()) - if err != nil { - logger.Fatal().Err(err).Msg("Failed to create Ethereum client.") - } + send, err := createSender(ctx, settings, logger) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to create sender.") + } - chainID, err := ethClient.ChainID(ctx) - if err != nil { - logger.Fatal().Err(err).Msg("Couldn't retrieve chain id.") - } - deviceDefinitionOnChainService := gateways.NewDeviceDefinitionOnChainService(settings, logger, ethClient, chainID, send, pdb.DBS) + ethClient, err := ethclient.Dial(settings.EthereumRPCURL.String()) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to create Ethereum client.") + } - return services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, deviceDefinitionOnChainService, datAPI, pdb.DBS, jp17vinAPI, carvxAPI, elevaAPI) + chainID, err := ethClient.ChainID(ctx) + if err != nil { + logger.Fatal().Err(err).Msg("Couldn't retrieve chain id.") } + deviceDefinitionOnChainService := gateways.NewDeviceDefinitionOnChainService(settings, logger, ethClient, chainID, send, pdb.DBS) + + return services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, deviceDefinitionOnChainService, datAPI, pdb.DBS, jp17vinAPI, carvxAPI, elevaAPI) }