From 433ae91dea926f19b790c5e228d3f9a09802450d Mon Sep 17 00:00:00 2001 From: James Reategui Date: Wed, 18 Jun 2025 18:42:37 -0400 Subject: [PATCH 1/4] implement carvx decoding api, pending use side --- internal/config/settings.go | 2 + internal/core/models/vin_decoding_models.go | 19 +++++++ .../infrastructure/gateways/carvx_vin_api.go | 56 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 internal/infrastructure/gateways/carvx_vin_api.go diff --git a/internal/config/settings.go b/internal/config/settings.go index 0b2fabff..efb50b31 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -53,6 +53,8 @@ type Settings struct { GoogleSheetsCredentials string `yaml:"GOOGLE_SHEETS_CREDENTIALS"` Japan17VINUser string `yaml:"JAPAN17_VIN_USER"` Japan17VINPassword string `yaml:"JAPAN17_VIN_PASSWORD"` + CarVxUserId string `yaml:"CARVX_USER_ID"` + CarVxAPIKey string `yaml:"CARVX_API_KEY"` } func (s *Settings) IsProd() bool { diff --git a/internal/core/models/vin_decoding_models.go b/internal/core/models/vin_decoding_models.go index 31de708c..3d3b80f9 100644 --- a/internal/core/models/vin_decoding_models.go +++ b/internal/core/models/vin_decoding_models.go @@ -413,3 +413,22 @@ type Japan17MMY struct { ModelName string `json:"modelName"` Year int `json:"year"` } + +// nolint +type CarVxResponse struct { + Data []struct { + Make string `json:"make"` + Model string `json:"model"` + Grade string `json:"grade"` + Body string `json:"body"` + Engine string `json:"engine"` + Drive string `json:"drive"` + Transmission string `json:"transmission"` + Fuel string `json:"fuel"` + ManufactureDate struct { + Year string `json:"year"` + Month string `json:"month"` + } `json:"manufacture_date"` + } `json:"data"` + Error string `json:"error"` +} diff --git a/internal/infrastructure/gateways/carvx_vin_api.go b/internal/infrastructure/gateways/carvx_vin_api.go new file mode 100644 index 00000000..6f4f2e43 --- /dev/null +++ b/internal/infrastructure/gateways/carvx_vin_api.go @@ -0,0 +1,56 @@ +package gateways + +import ( + "encoding/json" + "fmt" + "github.com/DIMO-Network/device-definitions-api/internal/config" + coremodels "github.com/DIMO-Network/device-definitions-api/internal/core/models" + "github.com/DIMO-Network/shared/pkg/http" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "io" + "time" +) + +type carVxVINAPI struct { + httpClient http.ClientWrapper + logger zerolog.Logger + settings *config.Settings +} + +type CarVxVINAPI interface { + GetVINInfo(chassisNumber string) (*coremodels.CarVxResponse, error) +} + +const carvxURL = "https://carvx.jp/api/v1/get-chassis-info" + +func NewCarVxVINAPI(logger zerolog.Logger, settings *config.Settings) CarVxVINAPI { + headers := map[string]string{ + "Carvx-User-Uid": settings.CarVxUserId, + "Carvx-Api-Key": settings.CarVxAPIKey, + } + hc, _ := http.NewClientWrapper(carvxURL, "", 15*time.Second, headers, true, http.WithRetry(2)) + return &carVxVINAPI{ + httpClient: hc, + logger: logger, + settings: settings, + } +} + +func (c *carVxVINAPI) GetVINInfo(chassisNumber string) (*coremodels.CarVxResponse, error) { + response, err := c.httpClient.ExecuteRequest(fmt.Sprintf("?chassis_number=%s", chassisNumber), "GET", nil) + if err != nil { + return nil, errors.Wrapf(err, "failed to decode chassis number info from carvx api: %s", chassisNumber) + } + defer response.Body.Close() //nolint + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + return nil, errors.Wrapf(err, "error reading response body from url %s", carvxURL) + } + v := &coremodels.CarVxResponse{} + err = json.Unmarshal(bodyBytes, v) + if err != nil { + return nil, errors.Wrapf(err, "error decoding response body from url %s", carvxURL) + } + return v, nil +} From 6c46d82912f983321edbb86ac453d8cf778ea2e6 Mon Sep 17 00:00:00 2001 From: James Reategui Date: Wed, 18 Jun 2025 20:48:35 -0400 Subject: [PATCH 2/4] implement usage and lint --- .../bulk_update_powertrain.go | 7 --- cmd/device-definitions-api/decode_vin.go | 13 ++++- internal/config/settings.go | 2 +- internal/core/models/vin_decoding_models.go | 1 + .../core/services/vin_decoding_service.go | 27 ++++++++- .../infrastructure/gateways/carvx_vin_api.go | 26 ++++++--- .../device_definition_on_chain_service.go | 2 +- .../gateways/mocks/carvx_vin_api_mock.go | 57 +++++++++++++++++++ 8 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 internal/infrastructure/gateways/mocks/carvx_vin_api_mock.go diff --git a/cmd/device-definitions-api/bulk_update_powertrain.go b/cmd/device-definitions-api/bulk_update_powertrain.go index ba0e202b..23d1a077 100644 --- a/cmd/device-definitions-api/bulk_update_powertrain.go +++ b/cmd/device-definitions-api/bulk_update_powertrain.go @@ -150,13 +150,6 @@ func (p *bulkUpdatePowertrain) Execute(ctx context.Context, _ *flag.FlagSet, _ . update, err := onChainSvc.Update(ctx, manufName, updateContract) if err != nil { fmt.Printf("%s: Error updating device definition: %v\n", definitionID, err) - if strings.Contains(err.Error(), "nonce too low:") { - time.Sleep(10 * time.Second) - update, err = onChainSvc.Update(ctx, manufName, updateContract) - if err != nil { - fmt.Printf("%s: Error updating device definition: %v\n", definitionID, err) - } - } return subcommands.ExitFailure } fmt.Printf("%s: Updated device definition trx id: %s\nWaiting 10 seconds before next update\n", definitionID, *update) diff --git a/cmd/device-definitions-api/decode_vin.go b/cmd/device-definitions-api/decode_vin.go index 3dd6975c..1a8c5510 100644 --- a/cmd/device-definitions-api/decode_vin.go +++ b/cmd/device-definitions-api/decode_vin.go @@ -36,6 +36,7 @@ type decodeVINCmd struct { japan17vin bool fromFile bool persistToDB bool + carvx bool } func (*decodeVINCmd) Name() string { return "decodevin" } @@ -43,7 +44,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|-from-file] ` + return `decodevin [-dat|-drivly|-vincario|-japan17vin|carvx|-from-file] ` } func (p *decodeVINCmd) SetFlags(f *flag.FlagSet) { @@ -51,6 +52,7 @@ func (p *decodeVINCmd) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.drivly, "drivly", false, "use drivly vin decoder") 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.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") } @@ -138,7 +140,14 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf fmt.Println(err.Error()) continue } - + fmt.Printf("VIN Response: %+v\n", vinInfo) + } + if p.carvx { + vinInfo, err = vinDecodingService.GetVIN(ctx, vin, dt, coremodels.CarVXVIN, country) + if err != nil { + fmt.Println(err.Error()) + continue + } fmt.Printf("VIN Response: %+v\n", vinInfo) } if p.japan17vin { diff --git a/internal/config/settings.go b/internal/config/settings.go index efb50b31..09917187 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -53,7 +53,7 @@ type Settings struct { GoogleSheetsCredentials string `yaml:"GOOGLE_SHEETS_CREDENTIALS"` Japan17VINUser string `yaml:"JAPAN17_VIN_USER"` Japan17VINPassword string `yaml:"JAPAN17_VIN_PASSWORD"` - CarVxUserId string `yaml:"CARVX_USER_ID"` + CarVxUserID string `yaml:"CAR_VX_USER_ID"` CarVxAPIKey string `yaml:"CARVX_API_KEY"` } diff --git a/internal/core/models/vin_decoding_models.go b/internal/core/models/vin_decoding_models.go index 3d3b80f9..2d3181b5 100644 --- a/internal/core/models/vin_decoding_models.go +++ b/internal/core/models/vin_decoding_models.go @@ -19,6 +19,7 @@ const ( AllProviders DecodeProviderEnum = "" TeslaProvider DecodeProviderEnum = "tesla" Japan17VIN DecodeProviderEnum = "japan17vin" + CarVXVIN DecodeProviderEnum = "carvxvin" ) type VINDecodingInfoData struct { diff --git a/internal/core/services/vin_decoding_service.go b/internal/core/services/vin_decoding_service.go index f6a3158f..d18379e7 100644 --- a/internal/core/services/vin_decoding_service.go +++ b/internal/core/services/vin_decoding_service.go @@ -38,6 +38,7 @@ type vinDecodingService struct { autoIsoAPIService gateways.AutoIsoAPIService DATGroupAPIService gateways.DATGroupAPIService japan17VINAPI gateways.Japan17VINAPI + carvxAPI gateways.CarVxVINAPI onChainSvc gateways.DeviceDefinitionOnChainService dbs func() *db.ReaderWriter } @@ -55,9 +56,9 @@ func (c vinDecodingService) GetVIN(ctx context.Context, vin string, dt *repoMode result := &coremodels.VINDecodingInfoData{} vin = strings.ToUpper(strings.TrimSpace(vin)) - // check for japan chasis - if (len(vin) < 17 && len(vin) > 10) || country == "JPN" { - provider = coremodels.Japan17VIN + // check for japan chasis if all providers + if provider == coremodels.AllProviders && (len(vin) < 17 && len(vin) > 10 || country == "JPN") { + provider = coremodels.CarVXVIN } else if !ValidateVIN(vin) { return nil, fmt.Errorf("invalid vin: %s", vin) } @@ -124,6 +125,24 @@ func (c vinDecodingService) GetVIN(ctx context.Context, vin string, dt *repoMode MetaData: null.JSONFrom(raw), Raw: raw, } + case coremodels.CarVXVIN: + info, raw, err := c.carvxAPI.GetVINInfo(vin) + if err != nil { + return nil, errors.Wrapf(err, "unable to decode vin: %s with carvx", vin) + } + yr, _ := strconv.Atoi(info.Data[0].ManufactureDate.Year) + result = &coremodels.VINDecodingInfoData{ + VIN: vin, + Make: info.Data[0].Make, + Model: info.Data[0].Model, + SubModel: info.Data[0].Drive + " " + info.Data[0].Transmission, + Year: int32(yr), + StyleName: info.Data[0].Drive + " " + info.Data[0].Transmission, + Source: coremodels.CarVXVIN, + MetaData: null.JSONFrom(raw), + Raw: raw, + FuelType: info.Data[0].Fuel, + } case coremodels.AutoIsoProvider: vinAutoIsoInfo, err := c.autoIsoAPIService.GetVIN(vin) if err != nil { @@ -202,6 +221,8 @@ func (c vinDecodingService) GetVIN(ctx context.Context, vin string, dt *repoMode } } } + + // todo try carvx and japan17vin if nothing from above } // could not decode anything if result == nil || result.Source == "" { diff --git a/internal/infrastructure/gateways/carvx_vin_api.go b/internal/infrastructure/gateways/carvx_vin_api.go index 6f4f2e43..974b2478 100644 --- a/internal/infrastructure/gateways/carvx_vin_api.go +++ b/internal/infrastructure/gateways/carvx_vin_api.go @@ -3,15 +3,17 @@ package gateways import ( "encoding/json" "fmt" + "io" + "time" + "github.com/DIMO-Network/device-definitions-api/internal/config" coremodels "github.com/DIMO-Network/device-definitions-api/internal/core/models" "github.com/DIMO-Network/shared/pkg/http" "github.com/pkg/errors" "github.com/rs/zerolog" - "io" - "time" ) +//go:generate mockgen -source carvx_vin_api.go -destination mocks/carvx_vin_api_mock.go -package mocks type carVxVINAPI struct { httpClient http.ClientWrapper logger zerolog.Logger @@ -19,14 +21,14 @@ type carVxVINAPI struct { } type CarVxVINAPI interface { - GetVINInfo(chassisNumber string) (*coremodels.CarVxResponse, error) + GetVINInfo(chassisNumber string) (*coremodels.CarVxResponse, []byte, error) } const carvxURL = "https://carvx.jp/api/v1/get-chassis-info" func NewCarVxVINAPI(logger zerolog.Logger, settings *config.Settings) CarVxVINAPI { headers := map[string]string{ - "Carvx-User-Uid": settings.CarVxUserId, + "Carvx-User-Uid": settings.CarVxUserID, "Carvx-Api-Key": settings.CarVxAPIKey, } hc, _ := http.NewClientWrapper(carvxURL, "", 15*time.Second, headers, true, http.WithRetry(2)) @@ -37,20 +39,26 @@ func NewCarVxVINAPI(logger zerolog.Logger, settings *config.Settings) CarVxVINAP } } -func (c *carVxVINAPI) GetVINInfo(chassisNumber string) (*coremodels.CarVxResponse, error) { +func (c *carVxVINAPI) GetVINInfo(chassisNumber string) (*coremodels.CarVxResponse, []byte, error) { response, err := c.httpClient.ExecuteRequest(fmt.Sprintf("?chassis_number=%s", chassisNumber), "GET", nil) if err != nil { - return nil, errors.Wrapf(err, "failed to decode chassis number info from carvx api: %s", chassisNumber) + return nil, nil, errors.Wrapf(err, "failed to decode chassis number info from carvx api: %s", chassisNumber) } defer response.Body.Close() //nolint bodyBytes, err := io.ReadAll(response.Body) if err != nil { - return nil, errors.Wrapf(err, "error reading response body from url %s", carvxURL) + return nil, nil, errors.Wrapf(err, "error reading response body from url %s", carvxURL) } v := &coremodels.CarVxResponse{} err = json.Unmarshal(bodyBytes, v) if err != nil { - return nil, errors.Wrapf(err, "error decoding response body from url %s", carvxURL) + return nil, nil, errors.Wrapf(err, "error decoding response body from url %s", carvxURL) + } + if len(v.Error) > 0 { + return nil, nil, errors.New(v.Error) + } + if len(v.Data) == 0 { + return nil, nil, errors.New("no data found") } - return v, nil + return v, bodyBytes, nil } diff --git a/internal/infrastructure/gateways/device_definition_on_chain_service.go b/internal/infrastructure/gateways/device_definition_on_chain_service.go index bf8a578a..28862c8f 100644 --- a/internal/infrastructure/gateways/device_definition_on_chain_service.go +++ b/internal/infrastructure/gateways/device_definition_on_chain_service.go @@ -414,7 +414,7 @@ func (e *deviceDefinitionOnChainService) Create(ctx context.Context, manufacture // Update on-chain device definition, only has basic validation that some fields be present. Requires existing tableland record to exist to update func (e *deviceDefinitionOnChainService) Update(ctx context.Context, manufacturerName string, input contracts.DeviceDefinitionUpdateInput) (*string, error) { - + // todo add retry logic to this call metrics.Success.With(prometheus.Labels{"method": TablelandRequests}).Inc() e.logger.Info().Msgf("OnChain Start Update for device definition %s. EthereumSendTransaction %t. payload: %+v", input.Id, e.settings.EthereumSendTransaction, input) diff --git a/internal/infrastructure/gateways/mocks/carvx_vin_api_mock.go b/internal/infrastructure/gateways/mocks/carvx_vin_api_mock.go new file mode 100644 index 00000000..d5351219 --- /dev/null +++ b/internal/infrastructure/gateways/mocks/carvx_vin_api_mock.go @@ -0,0 +1,57 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: carvx_vin_api.go +// +// Generated by this command: +// +// mockgen -source carvx_vin_api.go -destination mocks/carvx_vin_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" +) + +// MockCarVxVINAPI is a mock of CarVxVINAPI interface. +type MockCarVxVINAPI struct { + ctrl *gomock.Controller + recorder *MockCarVxVINAPIMockRecorder + isgomock struct{} +} + +// MockCarVxVINAPIMockRecorder is the mock recorder for MockCarVxVINAPI. +type MockCarVxVINAPIMockRecorder struct { + mock *MockCarVxVINAPI +} + +// NewMockCarVxVINAPI creates a new mock instance. +func NewMockCarVxVINAPI(ctrl *gomock.Controller) *MockCarVxVINAPI { + mock := &MockCarVxVINAPI{ctrl: ctrl} + mock.recorder = &MockCarVxVINAPIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCarVxVINAPI) EXPECT() *MockCarVxVINAPIMockRecorder { + return m.recorder +} + +// GetVINInfo mocks base method. +func (m *MockCarVxVINAPI) GetVINInfo(chassisNumber string) (*models.CarVxResponse, []byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVINInfo", chassisNumber) + ret0, _ := ret[0].(*models.CarVxResponse) + ret1, _ := ret[1].([]byte) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetVINInfo indicates an expected call of GetVINInfo. +func (mr *MockCarVxVINAPIMockRecorder) GetVINInfo(chassisNumber any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVINInfo", reflect.TypeOf((*MockCarVxVINAPI)(nil).GetVINInfo), chassisNumber) +} From ce29fd7e49178594f92aa0d7e6b2da3862da144c Mon Sep 17 00:00:00 2001 From: James Reategui Date: Wed, 18 Jun 2025 20:51:59 -0400 Subject: [PATCH 3/4] add secrets --- charts/device-definitions-api/templates/secret.yaml | 6 ++++++ internal/config/settings.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/charts/device-definitions-api/templates/secret.yaml b/charts/device-definitions-api/templates/secret.yaml index 6b7bddc4..56c1a3ef 100644 --- a/charts/device-definitions-api/templates/secret.yaml +++ b/charts/device-definitions-api/templates/secret.yaml @@ -89,6 +89,12 @@ spec: - remoteRef: key: {{ .Release.Namespace }}/definitions/17vin/password secretKey: JAPAN17_VIN_PASSWORD + - remoteRef: + key: {{ .Release.Namespace }}/definitions/carvx/userid + secretKey: CAR_VX_USER_ID + - remoteRef: + key: {{ .Release.Namespace }}/definitions/carvx/apikey + secretKey: CAR_VX_API_KEY secretStoreRef: kind: ClusterSecretStore name: aws-secretsmanager-secret-store diff --git a/internal/config/settings.go b/internal/config/settings.go index 09917187..16f5af54 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -54,7 +54,7 @@ type Settings struct { Japan17VINUser string `yaml:"JAPAN17_VIN_USER"` Japan17VINPassword string `yaml:"JAPAN17_VIN_PASSWORD"` CarVxUserID string `yaml:"CAR_VX_USER_ID"` - CarVxAPIKey string `yaml:"CARVX_API_KEY"` + CarVxAPIKey string `yaml:"CAR_VX_API_KEY"` } func (s *Settings) IsProd() bool { From f2ee907e034fa928839d87e6eb2925dc93458512 Mon Sep 17 00:00:00 2001 From: James Reategui Date: Wed, 18 Jun 2025 21:59:54 -0400 Subject: [PATCH 4/4] improve selection process for picking decoder --- cmd/device-definitions-api/decode_vin.go | 15 +- internal/api/api.go | 3 +- internal/core/queries/decode_vin.go | 6 +- internal/core/queries/decode_vin_test.go | 20 +- .../mocks/vin_decoding_service_mock.go | 10 +- .../core/services/vin_decoding_service.go | 372 +++++++++--------- .../services/vin_decoding_service_test.go | 33 +- .../infrastructure/gateways/carvx_vin_api.go | 8 +- 8 files changed, 235 insertions(+), 232 deletions(-) diff --git a/cmd/device-definitions-api/decode_vin.go b/cmd/device-definitions-api/decode_vin.go index 1a8c5510..4f728f51 100644 --- a/cmd/device-definitions-api/decode_vin.go +++ b/cmd/device-definitions-api/decode_vin.go @@ -108,7 +108,7 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf if wmi != nil { dbVin.ManufacturerName = wmi.ManufacturerName } - dt, err := models.DeviceTypes(models.DeviceTypeWhere.ID.EQ(common.DefaultDeviceType)).One(ctx, pdb.DBS().Reader) + _, err := models.DeviceTypes(models.DeviceTypeWhere.ID.EQ(common.DefaultDeviceType)).One(ctx, pdb.DBS().Reader) if err != nil { fmt.Println(err.Error()) return subcommands.ExitFailure @@ -116,7 +116,7 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf vinInfo := &coremodels.VINDecodingInfoData{VIN: vin} if p.datGroup { - vinInfo, err = vinDecodingService.GetVIN(ctx, vin, dt, coremodels.DATGroupProvider, country) + vinInfo, err = vinDecodingService.GetVIN(ctx, vin, coremodels.DATGroupProvider, country) // use the dat group service to decode if err != nil { fmt.Println(err.Error()) @@ -126,7 +126,7 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf fmt.Printf("\n\nVIN Response: %+v\n", vinInfo) } if p.drivly { - vinInfo, err = vinDecodingService.GetVIN(ctx, vin, dt, coremodels.DrivlyProvider, country) + vinInfo, err = vinDecodingService.GetVIN(ctx, vin, coremodels.DrivlyProvider, country) if err != nil { fmt.Println(err.Error()) continue @@ -135,7 +135,7 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf fmt.Printf("VIN Response: %+v\n", vinInfo) } if p.vincario { - vinInfo, err = vinDecodingService.GetVIN(ctx, vin, dt, coremodels.VincarioProvider, country) + vinInfo, err = vinDecodingService.GetVIN(ctx, vin, coremodels.VincarioProvider, country) if err != nil { fmt.Println(err.Error()) continue @@ -143,7 +143,7 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf fmt.Printf("VIN Response: %+v\n", vinInfo) } if p.carvx { - vinInfo, err = vinDecodingService.GetVIN(ctx, vin, dt, coremodels.CarVXVIN, country) + vinInfo, err = vinDecodingService.GetVIN(ctx, vin, coremodels.CarVXVIN, country) if err != nil { fmt.Println(err.Error()) continue @@ -151,7 +151,7 @@ func (p *decodeVINCmd) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf fmt.Printf("VIN Response: %+v\n", vinInfo) } if p.japan17vin { - vinInfo, err = vinDecodingService.GetVIN(ctx, vin, dt, coremodels.Japan17VIN, country) + vinInfo, err = vinDecodingService.GetVIN(ctx, vin, coremodels.Japan17VIN, country) if err != nil { fmt.Println(err.Error()) continue @@ -249,6 +249,7 @@ func instantiateVINDecodingSvc(ctx context.Context, settings *config.Settings, l drivlyAPI := gateways.NewDrivlyAPIService(settings) vincarioAPI := gateways.NewVincarioAPIService(settings, logger) jp17vinAPI := gateways.NewJapan17VINAPI(logger, settings) + carvxAPI := gateways.NewCarVxVINAPI(logger, settings) send, err := createSender(ctx, settings, logger) if err != nil { @@ -266,7 +267,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) + vinDecodingService := services.NewVINDecodingService(drivlyAPI, vincarioAPI, nil, logger, deviceDefinitionOnChainService, datAPI, pdb.DBS, jp17vinAPI, carvxAPI) return vinDecodingService } diff --git a/internal/api/api.go b/internal/api/api.go index 0f65945e..12932663 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -61,6 +61,7 @@ func Run(ctx context.Context, logger zerolog.Logger, settings *config.Settings, fuelAPIService := gateways.NewFuelAPIService(settings, &logger) autoIsoAPIService := gateways.NewAutoIsoAPIService(settings.AutoIsoAPIUid, settings.AutoIsoAPIKey) japan17VINAPI := gateways.NewJapan17VINAPI(&logger, settings) + carvxAPI := gateways.NewCarVxVINAPI(&logger, settings) registryInstance, err := contracts.NewRegistry(settings.EthereumRegistryAddress, ethClient) if err != nil { logger.Fatal().Err(err).Msg("Failed to create registry query instance.") @@ -74,7 +75,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) + vincDecodingService := services.NewVINDecodingService(drivlyAPIService, vincarioAPIService, autoIsoAPIService, &logger, ddOnChainService, datGroupWSService, pdb.DBS, japan17VINAPI, carvxAPI) powerTrainTypeService, err := services.NewPowerTrainTypeService("powertrain_type_rule.yaml", &logger, ddOnChainService) searchService := search.NewTypesenseAPIService(settings, &logger) if err != nil { diff --git a/internal/core/queries/decode_vin.go b/internal/core/queries/decode_vin.go index bf221ed3..f886b7ea 100644 --- a/internal/core/queries/decode_vin.go +++ b/internal/core/queries/decode_vin.go @@ -186,7 +186,7 @@ func (dc DecodeVINQueryHandler) Handle(ctx context.Context, query mediator.Messa return resp, nil } - dt, err := models.DeviceTypes(models.DeviceTypeWhere.ID.EQ(common.DefaultDeviceType)).One(ctx, dc.dbs().Reader) + _, err = models.DeviceTypes(models.DeviceTypeWhere.ID.EQ(common.DefaultDeviceType)).One(ctx, dc.dbs().Reader) if err != nil { metrics.InternalError.With(prometheus.Labels{"method": VinErrors}).Inc() return nil, errors.Wrap(err, "failed to get device_type") @@ -201,13 +201,13 @@ func (dc DecodeVINQueryHandler) Handle(ctx context.Context, query mediator.Messa dbWMI, err := models.Wmis(models.WmiWhere.Wmi.EQ(wmi)).One(ctx, dc.dbs().Reader) if err == nil && dbWMI != nil { if dbWMI.ManufacturerName == "Tesla" { - vinInfo, err = dc.vinDecodingService.GetVIN(ctx, vin.String(), dt, coremodels.TeslaProvider, qry.Country) + vinInfo, err = dc.vinDecodingService.GetVIN(ctx, vin.String(), coremodels.TeslaProvider, qry.Country) resp.Manufacturer = "Tesla" } } // not a tesla, regular decode path if vinInfo == nil || vinInfo.Model == "" { - vinInfo, err = dc.vinDecodingService.GetVIN(ctx, vin.String(), dt, coremodels.AllProviders, qry.Country) // this will try drivly first unless of japan + vinInfo, err = dc.vinDecodingService.GetVIN(ctx, vin.String(), coremodels.AllProviders, qry.Country) // this will try drivly first unless of japan } // if no luck decoding VIN, try buildingVinInfo from known data passed in, typically smartcar or software connections diff --git a/internal/core/queries/decode_vin_test.go b/internal/core/queries/decode_vin_test.go index 0153ec39..aac0f722 100644 --- a/internal/core/queries/decode_vin_test.go +++ b/internal/core/queries/decode_vin_test.go @@ -147,7 +147,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_WithExistingDD_UpdatesAt metaData, _ := json.Marshal(metaDataInfo) vinDecodingInfoData.MetaData = null.JSONFrom(metaData) definitionID := dd.ID - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo(vinDecodingInfoData.StyleName, vinDecodingInfoData.FuelType).Return("ICE") s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( buildTestTblDD(definitionID, dd.Model, int(dd.Year)), nil, nil) @@ -262,7 +262,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_CreatesDD_WithMismatchWM styleLevelPT := "PHEV" s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( nil, nil, nil) // should return nil b/c doesn't exist - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo(vinDecodingInfoData.StyleName, vinDecodingInfoData.FuelType).Return(styleLevelPT) trxHashHex := "0xa90868fe9364dbf41695b3b87e630f6455cfd63a4711f56b64f631b828c02b35" @@ -416,7 +416,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_CreatesDD() { styleLevelPT := "PHEV" s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( nil, nil, nil) // should return nil b/c doesn't exist - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo(vinDecodingInfoData.StyleName, vinDecodingInfoData.FuelType).Return(styleLevelPT) trxHashHex := "0xa90868fe9364dbf41695b3b87e630f6455cfd63a4711f56b64f631b828c02b35" @@ -530,7 +530,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_WithExistingDD_AndStyleA vinDecodingInfoData.MetaData = null.JSONFrom(metaData) definitionID := dd.ID - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo(vinDecodingInfoData.StyleName, vinDecodingInfoData.FuelType).Return("HEV") s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( buildTestTblDD(definitionID, dd.Model, int(dd.Year)), nil, nil) @@ -624,7 +624,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_WithExistingWMI() { vinDecodingInfoData.MetaData = null.JSONFrom(metaData) definitionID := dd.ID - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo(vinDecodingInfoData.StyleName, vinDecodingInfoData.FuelType).Return("HEV") s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( buildTestTblDD(definitionID, dd.Model, int(dd.Year)), nil, nil) @@ -677,7 +677,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_TeslaDecode() { definitionID := dd.ID - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.TeslaProvider, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.TeslaProvider, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo(vinDecodingInfoData.StyleName, vinDecodingInfoData.FuelType).Return("BEV") s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( buildTestTblDD(definitionID, dd.Model, dd.Year), nil, nil) @@ -775,7 +775,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_InvalidVINYear_AutoIso() Model: "Escape", } definitionID := "ford_escape_2017" - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo("", "").Return("ICE") // normally this would return "" s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( buildTestTblDD(definitionID, "Escape", 2021), nil, nil) @@ -817,7 +817,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_InvalidStyleName_AutoIso StyleName: "1", } definitionID := "ford_escape_2017" - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(vinDecodingInfoData, nil) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo("1", "").Return("ICE") s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( buildTestTblDD(definitionID, "Escape", 2017), nil, nil) @@ -857,7 +857,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Fail_DecodeErr() { _ = dbtesthelper.SetupCreateAutoPiIntegration(s.T(), s.pdb) _ = dbtesthelper.SetupCreateMake("Ford") - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(nil, fmt.Errorf("unable to decode")) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(nil, fmt.Errorf("unable to decode")) qryResult, err := s.queryHandler.Handle(s.ctx, &DecodeVINQuery{VIN: vin, Country: country}) assert.Nil(s.T(), qryResult) @@ -873,7 +873,7 @@ func (s *DecodeVINQueryHandlerSuite) TestHandle_Success_DecodeKnownFallback() { _ = dbtesthelper.SetupCreateWMI(s.T(), "1FM", dm.Name, s.pdb) definitionID := "ford_bronco_2022" - s.mockVINService.EXPECT().GetVIN(ctx, vin, gomock.Any(), coremodels.AllProviders, "USA").Times(1).Return(nil, fmt.Errorf("unable to decode")) + s.mockVINService.EXPECT().GetVIN(ctx, vin, coremodels.AllProviders, "USA").Times(1).Return(nil, fmt.Errorf("unable to decode")) s.mockPowerTrainTypeService.EXPECT().ResolvePowerTrainFromVinInfo("", "").Return("ICE") s.mockDeviceDefinitionOnChainService.EXPECT().GetDefinitionByID(gomock.Any(), definitionID).Return( diff --git a/internal/core/services/mocks/vin_decoding_service_mock.go b/internal/core/services/mocks/vin_decoding_service_mock.go index 95501481..806a2035 100644 --- a/internal/core/services/mocks/vin_decoding_service_mock.go +++ b/internal/core/services/mocks/vin_decoding_service_mock.go @@ -14,7 +14,6 @@ import ( reflect "reflect" models "github.com/DIMO-Network/device-definitions-api/internal/core/models" - models0 "github.com/DIMO-Network/device-definitions-api/internal/infrastructure/db/models" gomock "go.uber.org/mock/gomock" ) @@ -22,6 +21,7 @@ import ( type MockVINDecodingService struct { ctrl *gomock.Controller recorder *MockVINDecodingServiceMockRecorder + isgomock struct{} } // MockVINDecodingServiceMockRecorder is the mock recorder for MockVINDecodingService. @@ -42,16 +42,16 @@ func (m *MockVINDecodingService) EXPECT() *MockVINDecodingServiceMockRecorder { } // GetVIN mocks base method. -func (m *MockVINDecodingService) GetVIN(ctx context.Context, vin string, dt *models0.DeviceType, provider models.DecodeProviderEnum, country string) (*models.VINDecodingInfoData, error) { +func (m *MockVINDecodingService) GetVIN(ctx context.Context, vin string, provider models.DecodeProviderEnum, country string) (*models.VINDecodingInfoData, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVIN", ctx, vin, dt, provider, country) + ret := m.ctrl.Call(m, "GetVIN", ctx, vin, provider, country) ret0, _ := ret[0].(*models.VINDecodingInfoData) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVIN indicates an expected call of GetVIN. -func (mr *MockVINDecodingServiceMockRecorder) GetVIN(ctx, vin, dt, provider, country any) *gomock.Call { +func (mr *MockVINDecodingServiceMockRecorder) GetVIN(ctx, vin, provider, country any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVIN", reflect.TypeOf((*MockVINDecodingService)(nil).GetVIN), ctx, vin, dt, provider, country) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVIN", reflect.TypeOf((*MockVINDecodingService)(nil).GetVIN), ctx, vin, provider, country) } diff --git a/internal/core/services/vin_decoding_service.go b/internal/core/services/vin_decoding_service.go index d18379e7..3c12d883 100644 --- a/internal/core/services/vin_decoding_service.go +++ b/internal/core/services/vin_decoding_service.go @@ -11,24 +11,22 @@ import ( "strings" "time" + "github.com/DIMO-Network/shared/pkg/logfields" + "github.com/DIMO-Network/shared/pkg/db" vinutil "github.com/DIMO-Network/shared/pkg/vin" "github.com/volatiletech/null/v8" - "github.com/DIMO-Network/device-definitions-api/internal/core/common" - repoModel "github.com/DIMO-Network/device-definitions-api/internal/infrastructure/db/models" - "github.com/pkg/errors" - "github.com/rs/zerolog" - "github.com/tidwall/gjson" - coremodels "github.com/DIMO-Network/device-definitions-api/internal/core/models" "github.com/DIMO-Network/device-definitions-api/internal/infrastructure/gateways" + "github.com/pkg/errors" + "github.com/rs/zerolog" ) type VINDecodingService interface { // GetVIN decodes a vin using one of the providers passed in or if AllProviders applies an ordered logic. Only pass TeslaProvider if know it is a Tesla. - GetVIN(ctx context.Context, vin string, dt *repoModel.DeviceType, provider coremodels.DecodeProviderEnum, country string) (*coremodels.VINDecodingInfoData, error) + GetVIN(ctx context.Context, vin string, provider coremodels.DecodeProviderEnum, country string) (*coremodels.VINDecodingInfoData, error) } type vinDecodingService struct { @@ -45,26 +43,28 @@ type vinDecodingService struct { 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) VINDecodingService { + japan17VINAPI gateways.Japan17VINAPI, carvxAPI gateways.CarVxVINAPI) VINDecodingService { return &vinDecodingService{drivlyAPISvc: drivlyAPISvc, vincarioAPISvc: vincarioAPISvc, autoIsoAPIService: autoIso, - japan17VINAPI: japan17VINAPI, logger: logger, onChainSvc: onChainSvc, DATGroupAPIService: datGroupAPIService, dbs: dbs} + japan17VINAPI: japan17VINAPI, carvxAPI: carvxAPI, logger: logger, onChainSvc: onChainSvc, DATGroupAPIService: datGroupAPIService, dbs: dbs} } -func (c vinDecodingService) GetVIN(ctx context.Context, vin string, dt *repoModel.DeviceType, provider coremodels.DecodeProviderEnum, country string) (*coremodels.VINDecodingInfoData, error) { - +func (c vinDecodingService) GetVIN(ctx context.Context, vin string, provider coremodels.DecodeProviderEnum, country string) (*coremodels.VINDecodingInfoData, error) { const DefaultDefinitionID = "ford_escape_2020" result := &coremodels.VINDecodingInfoData{} 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") { - provider = coremodels.CarVXVIN + 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) { return nil, fmt.Errorf("invalid vin: %s", vin) } localLog := c.logger.With(). - Str("vin", vin). + Str(logfields.VIN, vin). + Str(logfields.FunctionName, "GetVIN"). Logger() if strings.HasPrefix(vin, "0SC") { @@ -76,160 +76,187 @@ func (c vinDecodingService) GetVIN(ctx context.Context, vin string, dt *repoMode return result, nil } - switch provider { - case coremodels.TeslaProvider: - v := vinutil.VIN(vin) - metadata := map[string]interface{}{ - "fuel_type": "electric", - "powertrain_type": coremodels.BEV.String(), - } - bytes, _ := json.Marshal(metadata) - result = &coremodels.VINDecodingInfoData{ - VIN: vin, - Year: int32(v.Year()), - Make: "Tesla", - Model: v.TeslaModel(), - Source: coremodels.TeslaProvider, - FuelType: "electric", - MetaData: null.JSONFrom(bytes), - } - case coremodels.DrivlyProvider: - vinDrivlyInfo, err := c.drivlyAPISvc.GetVINInfo(vin) - if err != nil { - return nil, errors.Wrapf(err, "unable to decode vin: %s with drivly", vin) - } - result, err = buildFromDrivly(vinDrivlyInfo) - if err != nil { - return nil, errors.Wrapf(err, "unable to decode vin: %s with drivly", vin) - } - case coremodels.VincarioProvider: - vinVincarioInfo, err := c.vincarioAPISvc.DecodeVIN(vin) - if err != nil { - return nil, errors.Wrapf(err, "unable to decode vin: %s with vincario", vin) - } - result, err = buildFromVincario(vinVincarioInfo) - if err != nil { - return nil, err - } - case coremodels.Japan17VIN: - mmy, raw, err := c.japan17VINAPI.GetVINInfo(vin) - if err != nil { - return nil, errors.Wrapf(err, "unable to decode vin: %s with japan17vin", vin) - } - result = &coremodels.VINDecodingInfoData{ - VIN: vin, - Make: mmy.ManufacturerName, - Model: mmy.ModelName, - Year: int32(mmy.Year), - Source: coremodels.Japan17VIN, - MetaData: null.JSONFrom(raw), - Raw: raw, - } - case coremodels.CarVXVIN: - info, raw, err := c.carvxAPI.GetVINInfo(vin) - if err != nil { - return nil, errors.Wrapf(err, "unable to decode vin: %s with carvx", vin) - } - yr, _ := strconv.Atoi(info.Data[0].ManufactureDate.Year) - result = &coremodels.VINDecodingInfoData{ - VIN: vin, - Make: info.Data[0].Make, - Model: info.Data[0].Model, - SubModel: info.Data[0].Drive + " " + info.Data[0].Transmission, - Year: int32(yr), - StyleName: info.Data[0].Drive + " " + info.Data[0].Transmission, - Source: coremodels.CarVXVIN, - MetaData: null.JSONFrom(raw), - Raw: raw, - FuelType: info.Data[0].Fuel, - } - case coremodels.AutoIsoProvider: - vinAutoIsoInfo, err := c.autoIsoAPIService.GetVIN(vin) - if err != nil { - return nil, errors.Wrapf(err, "unable to decode vin: %s with autoiso", vin) - } - result, err = buildFromAutoIso(vinAutoIsoInfo) - if err != nil { - return nil, err - } - case coremodels.DATGroupProvider: - // todo lookup country for two letter equiv - vinInfo, err := c.DATGroupAPIService.GetVINv2(vin, country) // try with Turkey - if err != nil { - return nil, errors.Wrapf(err, "unable to decode vin: %s with DATGroup", vin) - } - result, err = buildFromDATGroup(vinInfo) - if err != nil { - return nil, err - } - case coremodels.AllProviders: - // todo if tesla, just build from tesla and use model - - vinDrivlyInfo, err := c.drivlyAPISvc.GetVINInfo(vin) - if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode - unable decode vin with drivly") + if len(providersToTry) == 0 { + if provider == coremodels.AllProviders { + // fill in the list, future could do something country specific + providersToTry = append(providersToTry, coremodels.DrivlyProvider, coremodels.AutoIsoProvider, coremodels.VincarioProvider, coremodels.DATGroupProvider) } else { - result, err = buildFromDrivly(vinDrivlyInfo) + // use the specified override + providersToTry = append(providersToTry, provider) + } + } + var errFinal error // for later + + for _, p := range providersToTry { + // try all the options, but need to continue if get an error + // todo: break if decode works, continue if need to try with next + switch p { + case coremodels.TeslaProvider: + v := vinutil.VIN(vin) + metadata := map[string]interface{}{ + "fuel_type": "electric", + "powertrain_type": coremodels.BEV.String(), + } + bytes, _ := json.Marshal(metadata) + result = &coremodels.VINDecodingInfoData{ + VIN: vin, + Year: int32(v.Year()), + Make: "Tesla", + Model: v.TeslaModel(), + Source: coremodels.TeslaProvider, + FuelType: "electric", + MetaData: null.JSONFrom(bytes), + } + localLog.Info().Msgf("decoded with tesla: %+v", result) + return result, nil + case coremodels.DrivlyProvider: + vinDrivlyInfo, err := c.drivlyAPISvc.GetVINInfo(vin) if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with drivly") - } else { - metadata, err := common.BuildDeviceTypeAttributes(buildDrivlyVINInfoToUpdateAttr(vinDrivlyInfo), dt) - if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode - unable to build metadata attributes") - } - result.MetaData = metadata + errFinal = errors.Wrapf(err, "unable to decode vin: %s with drivly", vin) + continue } - } - - // if nothing from drivly, try autoiso - if result == nil || result.Source == "" { - autoIsoInfo, err := c.autoIsoAPIService.GetVIN(vin) + result, err = buildFromDrivly(vinDrivlyInfo) if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with autoiso") - } else { - result, err = buildFromAutoIso(autoIsoInfo) - if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode -could not build struct from autoiso data") - } + errFinal = errors.Wrapf(err, "unable to decode vin: %s with drivly", vin) + continue } - } - - // if nothing,try vincario - if result == nil || result.Source == "" { + return result, nil + case coremodels.VincarioProvider: vinVincarioInfo, err := c.vincarioAPISvc.DecodeVIN(vin) if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with vincario") - } else { - result, err = buildFromVincario(vinVincarioInfo) - if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode -could not build struct from vincario data") - } + errFinal = errors.Wrapf(err, "unable to decode vin: %s with vincario", vin) + continue } - } - - // if nothing from vincario, try DATGroup - if result == nil || result.Source == "" { - // idea: only accept WMI's for DATgroup that they have succesfully decoded in the past - datGroupInfo, err := c.DATGroupAPIService.GetVINv2(vin, country) + result, err = buildFromVincario(vinVincarioInfo) + if err != nil { + errFinal = err + continue + } + return result, nil + case coremodels.Japan17VIN: + mmy, raw, err := c.japan17VINAPI.GetVINInfo(vin) + if err != nil { + errFinal = errors.Wrapf(err, "unable to decode vin: %s with japan17vin", vin) + continue + } + result = &coremodels.VINDecodingInfoData{ + VIN: vin, + Make: mmy.ManufacturerName, + Model: mmy.ModelName, + Year: int32(mmy.Year), + Source: coremodels.Japan17VIN, + MetaData: null.JSONFrom(raw), + Raw: raw, + } + return result, nil + case coremodels.CarVXVIN: + info, raw, err := c.carvxAPI.GetVINInfo(vin) + if err != nil { + errFinal = errors.Wrapf(err, "unable to decode vin: %s with carvx", vin) + continue + } + yr, _ := strconv.Atoi(info.Data[0].ManufactureDate.Year) + result = &coremodels.VINDecodingInfoData{ + VIN: vin, + Make: info.Data[0].Make, + Model: info.Data[0].Model, + SubModel: info.Data[0].Drive + " " + info.Data[0].Transmission, + Year: int32(yr), + StyleName: info.Data[0].Drive + " " + info.Data[0].Transmission, + Source: coremodels.CarVXVIN, + MetaData: null.JSONFrom(raw), + Raw: raw, + FuelType: info.Data[0].Fuel, + } + return result, nil + case coremodels.AutoIsoProvider: + vinAutoIsoInfo, err := c.autoIsoAPIService.GetVIN(vin) if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with DATGroup") - } else { - result, err = buildFromDATGroup(datGroupInfo) - localLog.Info().Msgf("datgroup result: %+v", result) // temporary for debugging - if err != nil { - localLog.Warn().Err(err).Msg("AllProviders decode - could not build struct from DATGroup data") - } + errFinal = errors.Wrapf(err, "unable to decode vin: %s with autoiso", vin) + continue } + result, err = buildFromAutoIso(vinAutoIsoInfo) + if err != nil { + errFinal = err + continue + } + return result, nil + case coremodels.DATGroupProvider: + // todo lookup country for two letter equiv + vinInfo, err := c.DATGroupAPIService.GetVINv2(vin, country) // try with Turkey + if err != nil { + errFinal = errors.Wrapf(err, "unable to decode vin: %s with DATGroup", vin) + continue + } + result, err = buildFromDATGroup(vinInfo) + if err != nil { + errFinal = err + continue + } + return result, nil + case coremodels.AllProviders: + //vinDrivlyInfo, err := c.drivlyAPISvc.GetVINInfo(vin) + //if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode - unable decode vin with drivly") + //} else { + // result, err = buildFromDrivly(vinDrivlyInfo) + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with drivly") + // } else { + // metadata, err := common.BuildDeviceTypeAttributes(buildDrivlyVINInfoToUpdateAttr(vinDrivlyInfo), dt) + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode - unable to build metadata attributes") + // } + // result.MetaData = metadata + // } + //} + // + //// if nothing from drivly, try autoiso + //if result == nil || result.Source == "" { + // autoIsoInfo, err := c.autoIsoAPIService.GetVIN(vin) + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with autoiso") + // } else { + // result, err = buildFromAutoIso(autoIsoInfo) + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode -could not build struct from autoiso data") + // } + // } + //} + // + //// if nothing,try vincario + //if result == nil || result.Source == "" { + // vinVincarioInfo, err := c.vincarioAPISvc.DecodeVIN(vin) + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with vincario") + // } else { + // result, err = buildFromVincario(vinVincarioInfo) + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode -could not build struct from vincario data") + // } + // } + //} + // + //// if nothing from vincario, try DATGroup + //if result == nil || result.Source == "" { + // // idea: only accept WMI's for DATgroup that they have succesfully decoded in the past + // datGroupInfo, err := c.DATGroupAPIService.GetVINv2(vin, country) + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode -could not decode vin with DATGroup") + // } else { + // result, err = buildFromDATGroup(datGroupInfo) + // localLog.Info().Msgf("datgroup result: %+v", result) // temporary for debugging + // if err != nil { + // localLog.Warn().Err(err).Msg("AllProviders decode - could not build struct from DATGroup data") + // } + // } + //} } - - // todo try carvx and japan17vin if nothing from above } + // could not decode anything - if result == nil || result.Source == "" { - return nil, fmt.Errorf("could not decode from any provider for vin: %s", vin) - } - if result.Year == 0 { - return nil, fmt.Errorf("unable to decode vin: %s - year returned as 0", vin) + if errFinal != nil { + return nil, errFinal } return result, nil @@ -246,38 +273,6 @@ func ValidateVIN(vin string) bool { return regex.MatchString(vin) } -func buildDrivlyVINInfoToUpdateAttr(vinInfo *coremodels.DrivlyVINResponse) []*coremodels.UpdateDeviceTypeAttribute { - seekAttributes := map[string]string{ - // {device attribute, must match device_types.properties}: {vin info from drivly} - "mpg_city": "mpgCity", - "mpg_highway": "mpgHighway", - "mpg": "mpg", - "base_msrp": "msrpBase", - "fuel_tank_capacity_gal": "fuelTankCapacityGal", - "fuel_type": "fuel", - "wheelbase": "wheelbase", - "generation": "generation", - "number_of_doors": "doors", - "manufacturer_code": "manufacturerCode", - "driven_wheels": "drive", - } - marshal, _ := json.Marshal(vinInfo) - var udta []*coremodels.UpdateDeviceTypeAttribute - - for dtAttrKey, drivlyKey := range seekAttributes { - v := gjson.GetBytes(marshal, drivlyKey).String() - // if v valid, ok etc - if len(v) > 0 && v != "0" && v != "0.0000" { - udta = append(udta, &coremodels.UpdateDeviceTypeAttribute{ - Name: dtAttrKey, - Value: v, - }) - } - } - - return udta -} - func buildFromAutoIso(info *coremodels.AutoIsoVINResponse) (*coremodels.VINDecodingInfoData, error) { raw, _ := json.Marshal(info) if info == nil { @@ -415,6 +410,9 @@ func validateVinDecoding(vdi *coremodels.VINDecodingInfoData) error { if strings.Contains(vdi.Model, ",") || strings.Contains(vdi.Model, "/") { return fmt.Errorf("model contains invalid characters: %s", vdi.Model) } + if vdi.Source == "" { + return fmt.Errorf("vin source is empty") + } return nil } diff --git a/internal/core/services/vin_decoding_service_test.go b/internal/core/services/vin_decoding_service_test.go index 5817d35a..8889f22b 100644 --- a/internal/core/services/vin_decoding_service_test.go +++ b/internal/core/services/vin_decoding_service_test.go @@ -33,6 +33,7 @@ type VINDecodingServiceSuite struct { mockAutoIsoAPISvc *mock_gateways.MockAutoIsoAPIService mockDATGroupAPIService *mock_gateways.MockDATGroupAPIService mockJapan17VINAPI *mock_gateways.MockJapan17VINAPI + mockCarvxAPI *mock_gateways.MockCarVxVINAPI mockOnChainSvc *mock_gateways.MockDeviceDefinitionOnChainService vinDecodingService VINDecodingService @@ -60,10 +61,11 @@ func (s *VINDecodingServiceSuite) SetupTest() { s.mockAutoIsoAPISvc = mock_gateways.NewMockAutoIsoAPIService(s.ctrl) s.mockDATGroupAPIService = mock_gateways.NewMockDATGroupAPIService(s.ctrl) s.mockJapan17VINAPI = mock_gateways.NewMockJapan17VINAPI(s.ctrl) + s.mockCarvxAPI = mock_gateways.NewMockCarVxVINAPI(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.mockOnChainSvc, s.mockDATGroupAPIService, s.pdb.DBS, s.mockJapan17VINAPI, s.mockCarvxAPI) } func (s *VINDecodingServiceSuite) TearDownTest() { @@ -91,11 +93,12 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_Japan17VIN_Success() { ModelName: "Voxy", Year: 2022, } + s.mockCarvxAPI.EXPECT().GetVINInfo(vin).Times(1).Return(nil, nil, fmt.Errorf("unable to decode")) s.mockJapan17VINAPI.EXPECT().GetVINInfo(vin).Times(1).Return(vinInfoResp, []byte{0x1, 0x22}, nil) - dt := dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) - result, err := s.vinDecodingService.GetVIN(ctx, vin, dt, coremodels.AllProviders, country) + result, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.AllProviders, country) s.NoError(err) assert.Equal(s.T(), result.VIN, vin) @@ -134,9 +137,9 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_Drivly_Success() { } s.mockDrivlyAPISvc.EXPECT().GetVINInfo(vin).Times(1).Return(vinInfoResp, nil) - dt := dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) - result, err := s.vinDecodingService.GetVIN(ctx, vin, dt, coremodels.AllProviders, country) + result, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.AllProviders, country) s.NoError(err) assert.Equal(s.T(), result.VIN, vin) @@ -146,8 +149,8 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_Drivly_Success() { func (s *VINDecodingServiceSuite) Test_VINDecodingService_Tesla() { ctx := context.Background() const vin = "5YJ3E1EA2PF696023" - dt := dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) - result, err := s.vinDecodingService.GetVIN(ctx, vin, dt, coremodels.TeslaProvider, "USA") + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + result, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.TeslaProvider, "USA") s.NoError(err) assert.Equal(s.T(), result.VIN, vin) @@ -183,9 +186,9 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_Vincario_Success() { // vincario is the last fallback s.mockVincarioAPISvc.EXPECT().DecodeVIN(vin).Times(1).Return(vincarioResp, nil) - dt := dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) - result, err := s.vinDecodingService.GetVIN(ctx, vin, dt, coremodels.AllProviders, country) + result, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.AllProviders, country) s.NoError(err) assert.Equal(s.T(), result.VIN, vin) @@ -206,9 +209,9 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_AutoIso_Success() { s.mockDrivlyAPISvc.EXPECT().GetVINInfo(vin).Times(1).Return(nil, fmt.Errorf("unable to decode")) s.mockAutoIsoAPISvc.EXPECT().GetVIN(vin).Times(1).Return(vinInfoResp, nil) - dt := dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) - result, err := s.vinDecodingService.GetVIN(ctx, vin, dt, coremodels.AllProviders, country) + result, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.AllProviders, country) s.NoError(err) assert.Equal(s.T(), result.VIN, vin) @@ -220,13 +223,13 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_DD_Default_Success() { const vin = "0SCZZZ4M0KD018683" const country = "US" - dt := dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) dm := dbtesthelper.SetupCreateMake("Ford") dd := dbtesthelper.SetupCreateDeviceDefinition(s.T(), dm.Name, "Escape", 2020, s.pdb) s.mockOnChainSvc.EXPECT().GetDefinitionByID(ctx, dd.ID).Times(1).Return(dd, nil, nil) - result, err := s.vinDecodingService.GetVIN(ctx, vin, dt, coremodels.AllProviders, country) + result, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.AllProviders, country) s.NoError(err) assert.Equal(s.T(), result.VIN, vin) @@ -249,9 +252,9 @@ func (s *VINDecodingServiceSuite) Test_VINDecodingService_DATGroup_Success() { s.mockDATGroupAPIService.EXPECT().GetVINv2(vin, country).Times(1).Return(vinInfoResp, nil) - dt := dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) + _ = dbtesthelper.SetupCreateDeviceType(s.T(), s.pdb) - result, err := s.vinDecodingService.GetVIN(ctx, vin, dt, coremodels.DATGroupProvider, country) + result, err := s.vinDecodingService.GetVIN(ctx, vin, coremodels.DATGroupProvider, country) s.NoError(err) assert.Equal(s.T(), result.VIN, vin) diff --git a/internal/infrastructure/gateways/carvx_vin_api.go b/internal/infrastructure/gateways/carvx_vin_api.go index 974b2478..65cd4b44 100644 --- a/internal/infrastructure/gateways/carvx_vin_api.go +++ b/internal/infrastructure/gateways/carvx_vin_api.go @@ -16,7 +16,7 @@ import ( //go:generate mockgen -source carvx_vin_api.go -destination mocks/carvx_vin_api_mock.go -package mocks type carVxVINAPI struct { httpClient http.ClientWrapper - logger zerolog.Logger + logger *zerolog.Logger settings *config.Settings } @@ -26,7 +26,7 @@ type CarVxVINAPI interface { const carvxURL = "https://carvx.jp/api/v1/get-chassis-info" -func NewCarVxVINAPI(logger zerolog.Logger, settings *config.Settings) CarVxVINAPI { +func NewCarVxVINAPI(logger *zerolog.Logger, settings *config.Settings) CarVxVINAPI { headers := map[string]string{ "Carvx-User-Uid": settings.CarVxUserID, "Carvx-Api-Key": settings.CarVxAPIKey, @@ -55,10 +55,10 @@ func (c *carVxVINAPI) GetVINInfo(chassisNumber string) (*coremodels.CarVxRespons return nil, nil, errors.Wrapf(err, "error decoding response body from url %s", carvxURL) } if len(v.Error) > 0 { - return nil, nil, errors.New(v.Error) + return nil, nil, fmt.Errorf("%s", v.Error) } if len(v.Data) == 0 { - return nil, nil, errors.New("no data found") + return nil, nil, fmt.Errorf("no data found") } return v, bodyBytes, nil }