diff --git a/.env.example b/.env.example index b8e01287..5cd29632 100644 --- a/.env.example +++ b/.env.example @@ -4,12 +4,12 @@ DEBUG=True ALLOWED_HOSTS=0.0.0.0 SERVER_HOST=0.0.0.0 SERVER_PORT=8000 +SERVER_URL=http://localhost:8000 JWT_ACCESS_LIFESPAN=15 JWT_REFRESH_LIFESPAN=10080 HMAC_TIMESTAMP_AGE=5 ENVIRONMENT=local # local, staging, production SENTRY_DSN= -HOST_DOMAIN=http://localhost:8000 # Database Config DB_NAME=paycrest diff --git a/.github/workflows/atlas-migrate.yml b/.github/workflows/atlas-migrate.yml new file mode 100644 index 00000000..2de35fa1 --- /dev/null +++ b/.github/workflows/atlas-migrate.yml @@ -0,0 +1,25 @@ +name: Atlas Database Migrations + +on: + push: + branches: + - main + - stable + +jobs: + migrate: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'staging' || 'production' }} + steps: + - uses: actions/checkout@v4 + + - name: Install Atlas + run: | + curl -sSf https://atlasgo.sh | sh + echo "$HOME/.atlas/bin" >> $GITHUB_PATH + + - name: Run Migrations + run: | + atlas migrate apply \ + --dir "file://ent/migrate/migrations" \ + --url "${{ secrets.DB_URL }}" \ No newline at end of file diff --git a/README.md b/README.md index 8a80c72e..6aad3965 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,14 @@ docker-compose up -d chmod +x scripts/import_db.sh # run the script to seed db with sample configured sender & provider profile -./scripts/import_db.sh +./scripts/import_db.sh -h localhost ``` 3. Run our sandbox provision node and connect it to your local aggregator by following the [instructions here](https://paycrest.notion.site/run-sandbox-provision-node) Here, we’d make use of a demo provision node and connect it to our local aggregator -That's it! The server will now be running at http://localhost:8000. You can use an API testing tool like Postman or cURL to interact with the API. +That's it! The server will now be running at http://localhost:8000. You can use an API testing tool like Postman or cURL to interact with the Sender API using the sandbox API Key `11f93de0-d304-4498-8b7b-6cecbc5b2dd8`. ## Usage diff --git a/aggregator b/aggregator new file mode 100644 index 00000000..f67abd0a Binary files /dev/null and b/aggregator differ diff --git a/config/config.go b/config/config.go index b93e6990..bbb1a8c4 100644 --- a/config/config.go +++ b/config/config.go @@ -21,6 +21,8 @@ type Configuration struct { func SetupConfig() error { var configuration *Configuration + viper.AddConfigPath("../../../..") + viper.AddConfigPath("../../..") viper.AddConfigPath("../..") viper.AddConfigPath("..") viper.AddConfigPath(".") diff --git a/config/server.go b/config/server.go index f0a5c76e..3054850a 100644 --- a/config/server.go +++ b/config/server.go @@ -15,7 +15,7 @@ type ServerConfiguration struct { AllowedHosts string Environment string SentryDSN string - HostDomain string + ServerURL string RateLimitUnauthenticated int RateLimitAuthenticated int SlackWebhookURL string @@ -33,6 +33,7 @@ func ServerConfig() *ServerConfiguration { viper.SetDefault("RATE_LIMIT_UNAUTHENTICATED", 5) viper.SetDefault("RATE_LIMIT_AUTHENTICATED", 100) viper.SetDefault("SLACK_WEBHOOK_URL", "") + viper.SetDefault("SERVER_URL", "") return &ServerConfiguration{ Debug: viper.GetBool("DEBUG"), @@ -42,7 +43,7 @@ func ServerConfig() *ServerConfiguration { AllowedHosts: viper.GetString("ALLOWED_HOSTS"), Environment: viper.GetString("ENVIRONMENT"), SentryDSN: viper.GetString("SENTRY_DSN"), - HostDomain: viper.GetString("HOST_DOMAIN"), + ServerURL: viper.GetString("SERVER_URL"), RateLimitUnauthenticated: viper.GetInt("RATE_LIMIT_UNAUTHENTICATED"), RateLimitAuthenticated: viper.GetInt("RATE_LIMIT_AUTHENTICATED"), SlackWebhookURL: viper.GetString("SLACK_WEBHOOK_URL"), diff --git a/controllers/accounts/auth.go b/controllers/accounts/auth.go index 8ee1ad7b..d9dba88f 100644 --- a/controllers/accounts/auth.go +++ b/controllers/accounts/auth.go @@ -58,7 +58,7 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { tx, err := db.Client.Tx(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to create new user: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create new user", nil) return @@ -99,7 +99,7 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { user, err := userCreate.Save(ctx) if err != nil { _ = tx.Rollback() - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to save new user: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create new user", nil) return @@ -113,13 +113,16 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { SetExpiryAt(time.Now().Add(authConf.PasswordResetLifespan)). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to create verification token: %v", err) } if serverConf.Environment == "production" { if verificationToken != nil { if _, err := ctrl.emailService.SendVerificationEmail(ctx, verificationToken.Token, user.Email, user.FirstName); err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "UserID": user.ID, + }).Errorf("Failed to send verification email") } } } @@ -180,7 +183,10 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { Save(ctx) if err != nil { _ = tx.Rollback() - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "UserID": user.ID, + }).Errorf("Failed to create provider profile") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create new user", nil) return @@ -190,7 +196,11 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { _, _, err = ctrl.apiKeyService.GenerateAPIKey(ctx, tx, nil, provider) if err != nil { _ = tx.Rollback() - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "UserID": user.ID, + "ProviderID": provider.ID, + }).Errorf("Failed to create API key for provider") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create new user", nil) return @@ -205,7 +215,10 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { Save(ctx) if err != nil { _ = tx.Rollback() - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "UserID": user.ID, + }).Errorf("Failed to create sender profile") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create new user", nil) return @@ -215,7 +228,11 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { _, _, err = ctrl.apiKeyService.GenerateAPIKey(ctx, tx, sender, nil) if err != nil { _ = tx.Rollback() - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "UserID": user.ID, + "SenderID": sender.ID, + }).Errorf("Failed to create API key for sender") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create new user", nil) return @@ -223,7 +240,7 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { } if err := tx.Commit(); err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to commit transaction: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create new user", nil) return @@ -246,7 +263,7 @@ func (ctrl *AuthController) Register(ctx *gin.Context) { // Send Slack notification if serverConf.Environment == "production" { if err := ctrl.slackService.SendUserSignupNotification(user, scopes, providerCurrencies); err != nil { - logger.Errorf("failed to send Slack notification: %v", err) + logger.Errorf("Failed to send Slack notification: %v", err) } } } @@ -296,7 +313,7 @@ func (ctrl *AuthController) Login(ctx *gin.Context) { accessToken, refreshToken, err := token.GeneratePairJWT(user.ID.String(), user.Scope) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error : Failed to create token pair during login: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create token pair", nil, ) diff --git a/controllers/accounts/profile.go b/controllers/accounts/profile.go index c47b3ca7..d98c7ddc 100644 --- a/controllers/accounts/profile.go +++ b/controllers/accounts/profile.go @@ -80,7 +80,7 @@ func (ctrl *ProfileController) UpdateSenderProfile(ctx *gin.Context) { // save or update SenderOrderToken tx, err := storage.Client.Tx(ctx) if err != nil { - u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile init", nil) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) return } @@ -175,7 +175,7 @@ func (ctrl *ProfileController) UpdateSenderProfile(ctx *gin.Context) { return } } else { - u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile err:", nil) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) return } @@ -238,6 +238,15 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { } if payload.HostIdentifier != "" { + // Validate HTTPS protocol + if !u.IsValidHttpsUrl(payload.HostIdentifier) { + u.APIResponse(ctx, http.StatusBadRequest, "error", + "Host identifier must use HTTPS protocol and be a valid URL", types.ErrorData{ + Field: "HostIdentifier", + Message: "Please provide a valid URL starting with https://", + }) + return + } update.SetHostIdentifier(payload.HostIdentifier) } @@ -271,7 +280,10 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { QueryCurrencies(). All(ctx) if err != nil { - logger.Errorf("Failed to fetch existing currencies: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + }).Errorf("Failed to fetch existing currencies for provider") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch existing currencies", nil) return } @@ -348,11 +360,14 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { if ent.IsNotFound(err) { u.APIResponse(ctx, http.StatusBadRequest, "error", fmt.Sprintf("Token not supported - %s", tokenPayload.Symbol), nil) } else { - logger.Errorf("Failed to check token support: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": tokenPayload.Symbol, + }).Errorf("Failed to check token support during update") u.APIResponse( ctx, http.StatusInternalServerError, - "error", "Failed to update profile 3", + "error", "Failed to update profile", nil, ) } @@ -370,15 +385,17 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { if ent.IsNotFound(err) { u.APIResponse(ctx, http.StatusBadRequest, "error", "Currency not supported", nil) } else { - logger.Errorf("Failed to fetch currency: %v", err) - u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch currency", nil) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Currency": tokenPayload.Currency, + }).Errorf("Failed to fetch currency during update") + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) } return } - var rate decimal.Decimal if tokenPayload.ConversionRateType == providerordertoken.ConversionRateTypeFloating { - rate = currency.MarketRate.Add(tokenPayload.FloatingConversionRate) + rate := currency.MarketRate.Add(tokenPayload.FloatingConversionRate) percentDeviation := u.AbsPercentageDeviation(currency.MarketRate, rate) if percentDeviation.GreaterThan(orderConf.PercentDeviationFromMarketRate) { @@ -387,10 +404,34 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { } } + // Calculate rate from tokenPayload based on conversion type + var rate decimal.Decimal + if tokenPayload.ConversionRateType == providerordertoken.ConversionRateTypeFixed { + rate = tokenPayload.FixedConversionRate + } else { + rate = currency.MarketRate.Add(tokenPayload.FloatingConversionRate) + } + // See if token already exists for provider tx, err := storage.Client.Tx(ctx) if err != nil { - u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to start transaction", nil) + tx.Rollback() + logger.Errorf("Failed to start transaction: %v", err) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) + return + } + + // Handle slippage validation and default value + if tokenPayload.RateSlippage.IsZero() { + // Set default slippage to 0% if not provided + tokenPayload.RateSlippage = decimal.NewFromFloat(0) + } else if tokenPayload.RateSlippage.LessThan(decimal.NewFromFloat(0.1)) { + tx.Rollback() + u.APIResponse(ctx, http.StatusBadRequest, "error", "Rate slippage cannot be less than 0.1%", nil) + return + } else if rate.Mul(tokenPayload.RateSlippage.Div(decimal.NewFromFloat(100))).GreaterThan(currency.MarketRate.Mul(decimal.NewFromFloat(0.05))) { + tx.Rollback() + u.APIResponse(ctx, http.StatusBadRequest, "error", "Rate slippage is too high", nil) return } @@ -400,7 +441,9 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { providerordertoken.HasTokenWith(token.IDEQ(providerToken.ID)), providerordertoken.HasProviderWith(providerprofile.IDEQ(provider.ID)), providerordertoken.HasCurrencyWith(fiatcurrency.IDEQ(currency.ID)), + providerordertoken.NetworkEQ(tokenPayload.Network), ). + WithCurrency(). Only(ctx) if err != nil { if ent.IsNotFound(err) { @@ -415,50 +458,63 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { SetAddress(tokenPayload.Address). SetNetwork(tokenPayload.Network). SetProviderID(provider.ID). + SetRateSlippage(tokenPayload.RateSlippage). SetTokenID(providerToken.ID). SetCurrencyID(currency.ID). Save(ctx) if err != nil { tx.Rollback() - logger.Errorf("Failed to create token: %v", err) - u.APIResponse(ctx, http.StatusInternalServerError, "error", fmt.Sprintf("Failed to create token - %s", tokenPayload.Symbol), nil) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": tokenPayload.Symbol, + "Currency": tokenPayload.Currency, + }).Errorf("Failed to create token during update") + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) return } } else { tx.Rollback() - logger.Errorf("Failed to query token: %v", err) - u.APIResponse(ctx, http.StatusInternalServerError, "error", fmt.Sprintf("Failed to query token - %s", tokenPayload.Symbol), nil) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": tokenPayload.Symbol, + "Currency": tokenPayload.Currency, + }).Errorf("Failed to query token during update") + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) return } } else { + // TODO: Remove when dashboard allows rate slippage to be set + if tokenPayload.RateSlippage.IsZero() && orderToken.RateSlippage.GreaterThan(decimal.NewFromFloat(0)) { + tokenPayload.RateSlippage = orderToken.RateSlippage + } + // Token exists, update it - _, err = orderToken.Update(). + _, err := orderToken.Update(). + SetAddress(tokenPayload.Address). + SetNetwork(tokenPayload.Network). + SetRateSlippage(tokenPayload.RateSlippage). SetConversionRateType(tokenPayload.ConversionRateType). SetFixedConversionRate(tokenPayload.FixedConversionRate). SetFloatingConversionRate(tokenPayload.FloatingConversionRate). SetMaxOrderAmount(tokenPayload.MaxOrderAmount). SetMinOrderAmount(tokenPayload.MinOrderAmount). - SetAddress(tokenPayload.Address). - SetNetwork(tokenPayload.Network). Save(ctx) if err != nil { tx.Rollback() - logger.Errorf("Failed to update token: %v", err) - u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update token - "+tokenPayload.Symbol, nil) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": tokenPayload.Symbol, + "Currency": tokenPayload.Currency, + }).Errorf("Failed to update token during update") + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) return } } if err := tx.Commit(); err != nil { tx.Rollback() - u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to commit transaction", nil) - return - } - - rate, err = ctrl.priorityQueueService.GetProviderRate(ctx, provider, providerToken.Symbol, currency.Code) - if err != nil { - logger.Errorf("Failed to get rate for provider %s", provider.ID) - u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to get rate", nil) + logger.Errorf("Failed to commit transaction: %v", err) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) return } @@ -474,7 +530,12 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { ). All(ctx) if err != nil { - logger.Errorf("Failed to assign provider %s to buckets", provider.ID) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + "MinAmount": tokenPayload.MinOrderAmount, + "MaxAmount": tokenPayload.MaxOrderAmount, + }).Errorf("Failed to assign provider to buckets") } else { update.ClearProvisionBuckets() update.AddProvisionBuckets(buckets...) @@ -488,7 +549,10 @@ func (ctrl *ProfileController) UpdateProviderProfile(ctx *gin.Context) { _, err := update.Save(ctx) if err != nil { - logger.Errorf("Failed to commit update of provider profile: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + }).Errorf("Failed to commit update of provider profile") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update profile", nil) return } @@ -508,7 +572,7 @@ func (ctrl *ProfileController) GetSenderProfile(ctx *gin.Context) { user, err := sender.QueryUser().Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to fetch sender profile for user %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } @@ -516,7 +580,10 @@ func (ctrl *ProfileController) GetSenderProfile(ctx *gin.Context) { // Get API key apiKey, err := ctrl.apiKeyService.GetAPIKey(ctx, sender, nil) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "SenderID": sender.ID, + }).Errorf("Failed to fetch sender API key") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } @@ -531,7 +598,10 @@ func (ctrl *ProfileController) GetSenderProfile(ctx *gin.Context) { ). All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "SenderID": sender.ID, + }).Errorf("Failed to fetch sender order tokens") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } @@ -570,7 +640,10 @@ func (ctrl *ProfileController) GetSenderProfile(ctx *gin.Context) { if ent.IsNotFound(err) { // do nothing } else { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "SenderID": sender.ID, + }).Errorf("Failed to fetch linked providerf for sender") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } @@ -601,7 +674,7 @@ func (ctrl *ProfileController) GetProviderProfile(ctx *gin.Context) { user, err := provider.QueryUser().Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to fetch provider profile for user %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } @@ -609,7 +682,10 @@ func (ctrl *ProfileController) GetProviderProfile(ctx *gin.Context) { // Get currencies currencies, err := provider.QueryCurrencies().All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + }).Errorf("Failed to fetch currencies for provider") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } @@ -628,7 +704,10 @@ func (ctrl *ProfileController) GetProviderProfile(ctx *gin.Context) { } orderTokens, err := query.All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + }).Errorf("Failed to fetch order tokens for provider") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } @@ -643,6 +722,7 @@ func (ctrl *ProfileController) GetProviderProfile(ctx *gin.Context) { FloatingConversionRate: orderToken.FloatingConversionRate, MaxOrderAmount: orderToken.MaxOrderAmount, MinOrderAmount: orderToken.MinOrderAmount, + RateSlippage: orderToken.RateSlippage, Address: orderToken.Address, Network: orderToken.Network, } @@ -652,7 +732,10 @@ func (ctrl *ProfileController) GetProviderProfile(ctx *gin.Context) { // Get API key apiKey, err := ctrl.apiKeyService.GetAPIKey(ctx, nil, provider) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + }).Errorf("Failed to fetch provider API key") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to retrieve profile", nil) return } diff --git a/controllers/accounts/profile_test.go b/controllers/accounts/profile_test.go index 22b960ec..b5a03de9 100644 --- a/controllers/accounts/profile_test.go +++ b/controllers/accounts/profile_test.go @@ -18,7 +18,8 @@ import ( "github.com/gin-gonic/gin" "github.com/paycrest/aggregator/ent/enttest" - "github.com/paycrest/aggregator/ent/fiatcurrency" + "github.com/paycrest/aggregator/ent/migrate" + "github.com/paycrest/aggregator/ent/providerordertoken" "github.com/paycrest/aggregator/ent/providerprofile" "github.com/paycrest/aggregator/ent/senderordertoken" "github.com/paycrest/aggregator/ent/senderprofile" @@ -33,6 +34,7 @@ var testCtx = struct { user *ent.User providerProfile *ent.ProviderProfile token *ent.Token + orderToken *ent.ProviderOrderToken client types.RPCClient }{} @@ -77,14 +79,27 @@ func setup() error { return err } - provderProfile, err := test.CreateTestProviderProfile(map[string]interface{}{ + providerProfile, err := test.CreateTestProviderProfile(map[string]interface{}{ "user_id": testCtx.user.ID, "currency_id": currency.ID, }) if err != nil { return err } - testCtx.providerProfile = provderProfile + + testCtx.providerProfile = providerProfile + orderToken, err := test.AddProviderOrderTokenToProvider(map[string]interface{}{ + "fixed_conversion_rate": decimal.NewFromFloat(550), + "conversion_rate_type": "fixed", + "floating_conversion_rate": decimal.NewFromFloat(0), + "provider": testCtx.providerProfile, + "token_id": testCtx.token.ID, + "currency_id": currency.ID, + }) + if err != nil { + return err + } + testCtx.orderToken = orderToken return nil } @@ -94,6 +109,11 @@ func TestProfile(t *testing.T) { client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1") defer client.Close() + // Run schema migrations + if err := client.Schema.Create(context.Background(), migrate.WithGlobalUniqueID(true)); err != nil { + t.Fatal(err) + } + db.Client = client // Setup test data @@ -331,24 +351,28 @@ func TestProfile(t *testing.T) { }) t.Run("UpdateProviderProfile", func(t *testing.T) { - - t.Run("with all fields complete and check if it is active", func(t *testing.T) { - // Test partial update + profileUpdateRequest := func(payload types.ProviderProfilePayload) *httptest.ResponseRecorder { accessToken, _ := token.GenerateAccessJWT(testCtx.user.ID.String(), "provider") headers := map[string]string{ "Authorization": "Bearer " + accessToken, } + res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router) + assert.NoError(t, err) + return res + } + + t.Run("with all fields complete and check if it is active", func(t *testing.T) { + // Test partial update payload := types.ProviderProfilePayload{ TradingName: "My Trading Name", Currencies: []string{"KES"}, - HostIdentifier: "example.com", + HostIdentifier: "https://example.com", BusinessDocument: "https://example.com/business_doc.png", IdentityDocument: "https://example.com/national_id.png", IsAvailable: true, } - res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router) - assert.NoError(t, err) + res := profileUpdateRequest(payload) // Assert the response body assert.Equal(t, http.StatusOK, res.Code) @@ -366,17 +390,130 @@ func TestProfile(t *testing.T) { Only(context.Background()) assert.NoError(t, err) - assert.Contains(t, providerProfile.TradingName, payload.TradingName) - assert.Contains(t, providerProfile.HostIdentifier, payload.HostIdentifier) - // assert.Contains(t, providerProfile.Edges.Currency.Code, payload.Currency) - assert.Contains(t, providerProfile.BusinessDocument, payload.BusinessDocument) - assert.Contains(t, providerProfile.IdentityDocument, payload.IdentityDocument) + assert.Equal(t, payload.TradingName, providerProfile.TradingName) + assert.Equal(t, payload.HostIdentifier, providerProfile.HostIdentifier) + assert.Equal(t, payload.BusinessDocument, providerProfile.BusinessDocument) + assert.Equal(t, payload.IdentityDocument, providerProfile.IdentityDocument) assert.True(t, providerProfile.IsActive) // assert for currencies assert.Equal(t, len(providerProfile.Edges.Currencies), 1) assert.Equal(t, providerProfile.Edges.Currencies[0].Code, payload.Currencies[0]) }) + t.Run("with token rate slippage", func(t *testing.T) { + + t.Run("fails when rate slippage exceeds 5 percent of market rate", func(t *testing.T) { + payload := types.ProviderProfilePayload{ + TradingName: testCtx.providerProfile.TradingName, + HostIdentifier: testCtx.providerProfile.HostIdentifier, + Currencies: []string{"KES"}, + Tokens: []types.ProviderOrderTokenPayload{{ + Currency: testCtx.orderToken.Edges.Currency.Code, + Symbol: testCtx.orderToken.Edges.Token.Symbol, + Network: testCtx.orderToken.Network, + RateSlippage: decimal.NewFromFloat(25), // 25% slippage + }}, + } + res := profileUpdateRequest(payload) + assert.Equal(t, http.StatusBadRequest, res.Code) + + var response types.Response + err = json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Rate slippage is too high", response.Message) + }) + + t.Run("fails when rate slippage is less than 0.1", func(t *testing.T) { + payload := types.ProviderProfilePayload{ + TradingName: testCtx.providerProfile.TradingName, + HostIdentifier: testCtx.providerProfile.HostIdentifier, + Currencies: []string{"KES"}, + Tokens: []types.ProviderOrderTokenPayload{{ + Currency: testCtx.orderToken.Edges.Currency.Code, + Symbol: testCtx.orderToken.Edges.Token.Symbol, + Network: testCtx.orderToken.Network, + RateSlippage: decimal.NewFromFloat(0.09), // 0.09% slippage + }}, + } + res := profileUpdateRequest(payload) + assert.Equal(t, http.StatusBadRequest, res.Code) + + var response types.Response + err = json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Rate slippage cannot be less than 0.1%", response.Message) + }) + + t.Run("succeeds with valid rate slippage", func(t *testing.T) { + payload := types.ProviderProfilePayload{ + TradingName: testCtx.providerProfile.TradingName, + HostIdentifier: testCtx.providerProfile.HostIdentifier, + Currencies: []string{"KES"}, + Tokens: []types.ProviderOrderTokenPayload{{ + Currency: testCtx.orderToken.Edges.Currency.Code, + Symbol: testCtx.orderToken.Edges.Token.Symbol, + ConversionRateType: testCtx.orderToken.ConversionRateType, + FixedConversionRate: testCtx.orderToken.FixedConversionRate, + FloatingConversionRate: testCtx.orderToken.FloatingConversionRate, + MaxOrderAmount: testCtx.orderToken.MaxOrderAmount, + MinOrderAmount: testCtx.orderToken.MinOrderAmount, + Network: testCtx.orderToken.Network, + RateSlippage: decimal.NewFromFloat(5), // 5% slippage + }}, + } + res := profileUpdateRequest(payload) + assert.Equal(t, http.StatusOK, res.Code) + + var response types.Response + err := json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Profile updated successfully", response.Message) + + // Verify the rate slippage was saved correctly + providerToken, err := db.Client.ProviderOrderToken. + Query(). + Where( + providerordertoken.HasProviderWith(providerprofile.IDEQ(testCtx.providerProfile.ID)), + ). + Only(context.Background()) + + assert.NoError(t, err) + assert.Equal(t, decimal.NewFromFloat(5), providerToken.RateSlippage) + }) + + // TODO: restore when dashboard has been updated + // t.Run("defaults to 0%% slippage when not specified", func(t *testing.T) { + // payload := types.ProviderProfilePayload{ + // TradingName: testCtx.providerProfile.TradingName, + // HostIdentifier: testCtx.providerProfile.HostIdentifier, + // Currencies: []string{"KES"}, + // Tokens: []types.ProviderOrderTokenPayload{{ + // Currency: testCtx.orderToken.Edges.Currency.Code, + // Symbol: testCtx.orderToken.Edges.Token.Symbol, + // ConversionRateType: testCtx.orderToken.ConversionRateType, + // FixedConversionRate: testCtx.orderToken.FixedConversionRate, + // FloatingConversionRate: testCtx.orderToken.FloatingConversionRate, + // MaxOrderAmount: testCtx.orderToken.MaxOrderAmount, + // MinOrderAmount: testCtx.orderToken.MinOrderAmount, + // Network: testCtx.orderToken.Network, + // }}, + // } + // res := profileUpdateRequest(payload) + // assert.Equal(t, http.StatusOK, res.Code) + + // // Verify the rate slippage defaulted to 0% + // providerToken, err := db.Client.ProviderOrderToken. + // Query(). + // Where( + // providerordertoken.HasProviderWith(providerprofile.IDEQ(testCtx.providerProfile.ID)), + // ). + // Only(context.Background()) + + // assert.NoError(t, err) + // assert.Equal(t, decimal.NewFromFloat(0), providerToken.RateSlippage) + // }) + }) + t.Run("with visibility", func(t *testing.T) { // Test partial update accessToken, _ := token.GenerateAccessJWT(testCtx.user.ID.String(), "provider") @@ -581,11 +718,89 @@ func TestProfile(t *testing.T) { }) }) + + t.Run("HostIdentifier URL validation", func(t *testing.T) { + accessToken, _ := token.GenerateAccessJWT(testCtx.user.ID.String(), "provider") + headers := map[string]string{ + "Authorization": "Bearer " + accessToken, + } + + t.Run("fails for HTTP URL", func(t *testing.T) { + payload := types.ProviderProfilePayload{ + TradingName: "Paycrest Profile", + HostIdentifier: "http://example.com", + Currencies: []string{"KES"}, + } + + res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.Code) + var response types.Response + err = json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Host identifier must use HTTPS protocol and be a valid URL", response.Message) + assert.NotNil(t, response.Data, "Response data should not be nil") + }) + + t.Run("fails for malformed URL", func(t *testing.T) { + payload := types.ProviderProfilePayload{ + TradingName: "Paycrest Profile", + HostIdentifier: "not-a-valid-url", + Currencies: []string{"KES"}, + } + + res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.Code) + var response types.Response + err = json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Host identifier must use HTTPS protocol and be a valid URL", response.Message) + }) + + t.Run("fails for URL without host", func(t *testing.T) { + payload := types.ProviderProfilePayload{ + TradingName: "Paycrest Profile", + HostIdentifier: "https://", + Currencies: []string{"KES"}, + } + + res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.Code) + var response types.Response + err = json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Host identifier must use HTTPS protocol and be a valid URL", response.Message) + }) + + t.Run("succeeds with valid HTTPS URL", func(t *testing.T) { + payload := types.ProviderProfilePayload{ + TradingName: "Paycrest Profile", + HostIdentifier: "https://example.com", + Currencies: []string{"KES"}, + } + + res, err := test.PerformRequest(t, "PATCH", "/settings/provider", payload, headers, router) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, res.Code) + var response types.Response + err = json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Profile updated successfully", response.Message) + providerProfile, err := db.Client.ProviderProfile. + Query(). + Where(providerprofile.HasUserWith(user.ID(testCtx.user.ID))). + Only(context.Background()) + assert.NoError(t, err) + assert.Equal(t, "https://example.com", providerProfile.HostIdentifier) + }) + }) }) t.Run("GetSenderProfile", func(t *testing.T) { testUser, err := test.CreateTestUser(map[string]interface{}{ - "email": "hello@test.com", + "email": "hello2@test.com", "scope": "sender", }) assert.NoError(t, err) @@ -627,6 +842,49 @@ func TestProfile(t *testing.T) { }) + t.Run("GetSenderProfile", func(t *testing.T) { + testUser, err := test.CreateTestUser(map[string]interface{}{ + "email": "hello@test.com", + "scope": "sender", + }) + assert.NoError(t, err) + + sender, err := test.CreateTestSenderProfile(map[string]interface{}{ + "domain_whitelist": []string{"mydomain.com"}, + "user_id": testUser.ID, + }) + assert.NoError(t, err) + + apiKeyService := services.NewAPIKeyService() + _, _, err = apiKeyService.GenerateAPIKey( + context.Background(), + nil, + sender, + nil, + ) + assert.NoError(t, err) + + accessToken, _ := token.GenerateAccessJWT(testUser.ID.String(), "sender") + headers := map[string]string{ + "Authorization": "Bearer " + accessToken, + } + res, err := test.PerformRequest(t, "GET", "/settings/sender", nil, headers, router) + assert.NoError(t, err) + + // Assert the response body + assert.Equal(t, http.StatusOK, res.Code) + var response struct { + Data types.SenderProfileResponse + Message string + } + err = json.Unmarshal(res.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Profile retrieved successfully", response.Message) + assert.NotNil(t, response.Data, "response.Data is nil") + assert.Greater(t, len(response.Data.Tokens), 0) + assert.Contains(t, response.Data.WebhookURL, "https://example.com") + }) + t.Run("GetProviderProfile", func(t *testing.T) { t.Run("with currency filter", func(t *testing.T) { ctx := context.Background() @@ -649,29 +907,6 @@ func TestProfile(t *testing.T) { Exec(ctx) assert.NoError(t, err) - // Retrieve the pre-created KES fiat currency (from setup) - kes, err := db.Client.FiatCurrency. - Query(). - Where(fiatcurrency.CodeEQ("KES")). - Only(ctx) - assert.NoError(t, err) - - // Create a provider order token for KES - _, err = db.Client.ProviderOrderToken. - Create(). - SetProviderID(testCtx.providerProfile.ID). - SetTokenID(testCtx.token.ID). - SetCurrencyID(kes.ID). - SetConversionRateType("floating"). - SetFixedConversionRate(decimal.NewFromInt(0)). - SetFloatingConversionRate(decimal.NewFromInt(1)). - SetMaxOrderAmount(decimal.NewFromInt(100)). - SetMinOrderAmount(decimal.NewFromInt(1)). - SetAddress("address_kes"). - SetNetwork("polygon"). - Save(ctx) - assert.NoError(t, err) - // Create a provider order token for USD _, err = db.Client.ProviderOrderToken. Create(). @@ -685,6 +920,7 @@ func TestProfile(t *testing.T) { SetMinOrderAmount(decimal.NewFromInt(10)). SetAddress("address_usd"). SetNetwork("polygon"). + SetRateSlippage(decimal.NewFromInt(0)). Save(ctx) assert.NoError(t, err) diff --git a/controllers/index.go b/controllers/index.go index 1c22ab71..b6a2f157 100644 --- a/controllers/index.go +++ b/controllers/index.go @@ -1,6 +1,7 @@ package controllers import ( + "encoding/hex" "fmt" "net/http" "slices" @@ -19,7 +20,8 @@ import ( "github.com/paycrest/aggregator/ent/providerprofile" tokenEnt "github.com/paycrest/aggregator/ent/token" svc "github.com/paycrest/aggregator/services" - "github.com/paycrest/aggregator/services/kyc" + kycErrors "github.com/paycrest/aggregator/services/kyc/errors" + "github.com/paycrest/aggregator/services/kyc/smile" orderSvc "github.com/paycrest/aggregator/services/order" "github.com/paycrest/aggregator/storage" "github.com/paycrest/aggregator/types" @@ -27,11 +29,13 @@ import ( "github.com/paycrest/aggregator/utils/logger" "github.com/shopspring/decimal" + "github.com/ethereum/go-ethereum/crypto" "github.com/gin-gonic/gin" ) var cryptoConf = config.CryptoConfig() -var serverConf = config.ServerConfig() + +// var serverConf = config.ServerConfig() var identityConf = config.IdentityConfig() // Controller is the default controller for other endpoints @@ -39,7 +43,7 @@ type Controller struct { orderService types.OrderService priorityQueueService *svc.PriorityQueueService receiveAddressService *svc.ReceiveAddressService - kycService kyc.KYCProvider + kycService types.KYCProvider } // NewController creates a new instance of AuthController with injected services @@ -48,7 +52,7 @@ func NewController() *Controller { orderService: orderSvc.NewOrderEVM(), priorityQueueService: svc.NewPriorityQueueService(), receiveAddressService: svc.NewReceiveAddressService(), - kycService: kyc.NewSmileIDService(), + kycService: smile.NewSmileIDService(), } } @@ -60,9 +64,10 @@ func (ctrl *Controller) GetFiatCurrencies(ctx *gin.Context) { Where(fiatcurrency.IsEnabledEQ(true)). All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to fetch fiat currencies: %v", err) + u.APIResponse(ctx, http.StatusBadRequest, "error", - "Failed to fetch FiatCurrencies", err.Error()) + "Failed to fetch FiatCurrencies", fmt.Sprintf("%v", err)) return } @@ -93,7 +98,7 @@ func (ctrl *Controller) GetInstitutionsByCurrency(ctx *gin.Context) { )). All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to fetch institutions: %v", err) u.APIResponse(ctx, http.StatusBadRequest, "error", "Failed to fetch institutions", nil) return @@ -122,7 +127,7 @@ func (ctrl *Controller) GetTokenRate(ctx *gin.Context) { ). First(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to fetch token rate: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch token rate", nil) return } @@ -140,7 +145,7 @@ func (ctrl *Controller) GetTokenRate(ctx *gin.Context) { ). Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to fetch token rate: %v", err) u.APIResponse(ctx, http.StatusBadRequest, "error", fmt.Sprintf("Fiat currency %s is not supported", strings.ToUpper(ctx.Param("fiat"))), nil) return } @@ -208,7 +213,14 @@ func (ctrl *Controller) GetTokenRate(ctx *gin.Context) { } parts := strings.Split(providerData, ":") if len(parts) != 5 { - logger.Errorf("GetTokenRate.InvalidProviderData: %v", providerData) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderData": providerData, + "Token": token.Symbol, + "Currency": currency.Code, + "MinAmount": minAmount, + "MaxAmount": maxAmount, + }).Errorf("GetTokenRate.InvalidProviderData: %v", providerData) continue } @@ -276,7 +288,7 @@ func (ctrl *Controller) GetSupportedTokens(ctx *gin.Context) { // Execute query tokens, err := query.All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to fetch tokens: error: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch tokens", nil) return } @@ -306,7 +318,11 @@ func (ctrl *Controller) VerifyAccount(ctx *gin.Context) { var payload types.VerifyAccountRequest if err := ctx.ShouldBindJSON(&payload); err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Institution": payload.Institution, + "AccountIdentifier": payload.AccountIdentifier, + }).Errorf("Failed to validate payload when verifying account") u.APIResponse(ctx, http.StatusBadRequest, "error", "Failed to validate payload", u.GetErrorData(err)) return @@ -322,7 +338,11 @@ func (ctrl *Controller) VerifyAccount(ctx *gin.Context) { ). Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Institution": payload.Institution, + "AccountIdentifier": payload.AccountIdentifier, + }).Errorf("Failed to validate payload when verifying account") u.APIResponse(ctx, http.StatusBadRequest, "error", "Failed to validate payload", []types.ErrorData{{ Field: "Institution", Message: "Institution is not supported", @@ -350,7 +370,7 @@ func (ctrl *Controller) VerifyAccount(ctx *gin.Context) { All(ctx) if err != nil { u.APIResponse(ctx, http.StatusBadRequest, "error", - "Failed to verify account", err.Error()) + "Failed to verify account", fmt.Sprintf("%v", err)) return } @@ -373,7 +393,11 @@ func (ctrl *Controller) VerifyAccount(ctx *gin.Context) { } if err != nil { - logger.Errorf("Failed to verify account: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Institution": payload.Institution, + "AccountIdentifier": payload.AccountIdentifier, + }).Errorf("Failed to verify account") u.APIResponse(ctx, http.StatusServiceUnavailable, "error", "Failed to verify account", nil) return } @@ -408,7 +432,11 @@ func (ctrl *Controller) GetLockPaymentOrderStatus(ctx *gin.Context) { WithTransactions(). All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": orderID, + "ChainID": chainID, + }).Errorf("Failed to fetch locked order status") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch order status", nil) return } @@ -482,7 +510,11 @@ func (ctrl *Controller) CreateLinkedAddress(ctx *gin.Context) { var payload types.NewLinkedAddressRequest if err := ctx.ShouldBindJSON(&payload); err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Institution": payload.Institution, + "AccountIdentifier": payload.AccountIdentifier, + }).Errorf("Failed to validate payload when creating linked address") u.APIResponse(ctx, http.StatusBadRequest, "error", "Failed to validate payload", u.GetErrorData(err)) return @@ -493,7 +525,7 @@ func (ctrl *Controller) CreateLinkedAddress(ctx *gin.Context) { // Generate smart account address, salt, err := ctrl.receiveAddressService.CreateSmartAddress(ctx, nil, nil) if err != nil { - logger.Errorf("error: %v", err) + logger.Errorf("Error: Failed to create linked address: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create linked address", nil) return } @@ -509,7 +541,12 @@ func (ctrl *Controller) CreateLinkedAddress(ctx *gin.Context) { SetOwnerAddress(ownerAddress.(string)). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Institution": payload.Institution, + "OwnerAddress": ownerAddress, + "Address": address, + }).Errorf("Failed to set linked address") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to create linked address", nil) return } @@ -540,7 +577,10 @@ func (ctrl *Controller) GetLinkedAddress(ctx *gin.Context) { u.APIResponse(ctx, http.StatusNotFound, "error", "Linked address not found", nil) return } else { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OwnerAddress": owner_address, + }).Errorf("Failed to fetch linked address") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch linked address", nil) return } @@ -552,7 +592,11 @@ func (ctrl *Controller) GetLinkedAddress(ctx *gin.Context) { WithFiatCurrency(). Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OwnerAddress": owner_address, + "LinkedAddressInstitution": linkedAddress.Institution, + }).Errorf("Failed to fetch linked address") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch linked address", nil) return } @@ -589,7 +633,10 @@ func (ctrl *Controller) GetLinkedAddressTransactions(ctx *gin.Context) { u.APIResponse(ctx, http.StatusNotFound, "error", "Linked address not found", nil) return } else { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linked_address, + }).Errorf("Failed to fetch linked address") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch linked address", nil) return } @@ -603,7 +650,12 @@ func (ctrl *Controller) GetLinkedAddressTransactions(ctx *gin.Context) { count, err := paymentOrderQuery.Count(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linked_address, + "LinkedAddressID": linkedAddress.ID, + "LinkedAddressOwnerAddress": linkedAddress.OwnerAddress, + }).Errorf("Failed to count payment orders for linked address") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch transactions", nil) return } @@ -617,7 +669,12 @@ func (ctrl *Controller) GetLinkedAddressTransactions(ctx *gin.Context) { }). All(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linked_address, + "LinkedAddressID": linkedAddress.ID, + "LinkedAddressOwnerAddress": linkedAddress.OwnerAddress, + }).Errorf("Failed to fetch fetch payment orders for linked address") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch transactions", nil) return } @@ -631,7 +688,13 @@ func (ctrl *Controller) GetLinkedAddressTransactions(ctx *gin.Context) { WithFiatCurrency(). Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linked_address, + "LinkedAddressID": linkedAddress.ID, + "LinkedAddressOwnerAddress": linkedAddress.OwnerAddress, + "PaymentOrderID": paymentOrder.ID, + }).Errorf("Failed to get institution for linked address") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch payment orders", nil) return } @@ -667,9 +730,39 @@ func (ctrl *Controller) GetLinkedAddressTransactions(ctx *gin.Context) { } +// verifyWalletSignature verifies the Ethereum signature for wallet verification +func (ctrl *Controller) verifyWalletSignature(walletAddress, signature, nonce string) error { + sig, err := hex.DecodeString(signature) + if err != nil { + return fmt.Errorf("invalid signature: signature is not in the correct format") + } + if len(sig) != 65 { + return fmt.Errorf("invalid signature: signature length is not correct") + } + if sig[64] != 27 && sig[64] != 28 { + return fmt.Errorf("invalid signature: invalid recovery ID") + } + sig[64] -= 27 + + message := fmt.Sprintf("I accept the KYC Policy and hereby request an identity verification check for %s with nonce %s", walletAddress, nonce) + prefix := "\x19Ethereum Signed Message:\n" + fmt.Sprint(len(message)) + hash := crypto.Keccak256Hash([]byte(prefix + message)) + + sigPublicKeyECDSA, err := crypto.SigToPub(hash.Bytes(), sig) + if err != nil { + return fmt.Errorf("invalid signature") + } + recoveredAddress := crypto.PubkeyToAddress(*sigPublicKeyECDSA) + if !strings.EqualFold(recoveredAddress.Hex(), walletAddress) { + return fmt.Errorf("invalid signature") + } + + return nil +} + // RequestIDVerification controller requests identity verification details func (ctrl *Controller) RequestIDVerification(ctx *gin.Context) { - var payload kyc.NewIDVerificationRequest + var payload types.VerificationRequest if err := ctx.ShouldBindJSON(&payload); err != nil { u.APIResponse(ctx, http.StatusBadRequest, "error", @@ -677,32 +770,57 @@ func (ctrl *Controller) RequestIDVerification(ctx *gin.Context) { return } + // Verify signature before proceeding + if err := ctrl.verifyWalletSignature(payload.WalletAddress, payload.Signature, payload.Nonce); err != nil { + u.APIResponse(ctx, http.StatusBadRequest, "error", "Invalid signature", fmt.Sprintf("%v", err)) + return + } + response, err := ctrl.kycService.RequestVerification(ctx, payload) if err != nil { - switch err.Error() { - case "invalid signature", "invalid signature: signature is not in the correct format", - "invalid signature: signature length is not correct", - "invalid signature: invalid recovery ID": - u.APIResponse(ctx, http.StatusBadRequest, "error", "Invalid signature", err.Error()) - return - case "signature already used for identity verification": + switch e := err.(type) { + case kycErrors.ErrSignatureAlreadyUsed: u.APIResponse(ctx, http.StatusBadRequest, "error", "Signature already used for identity verification", nil) return - case "this account has already been successfully verified": - u.APIResponse(ctx, http.StatusBadRequest, "success", "Failed to request identity verification", err.Error()) + case kycErrors.ErrAlreadyVerified: + u.APIResponse(ctx, http.StatusBadRequest, "success", "Failed to request identity verification", e.Error()) return - case "failed to request identity verification: couldn't reach identity provider": + case kycErrors.ErrProviderUnreachable: + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", e.Err), + "WalletAddress": payload.WalletAddress, + "Nonce": payload.Nonce, + }).Errorf("Failed to reach identity provider") u.APIResponse(ctx, http.StatusServiceUnavailable, "error", "Failed to request identity verification", "Couldn't reach identity provider") return + case kycErrors.ErrProviderResponse: + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", e.Err), + "WalletAddress": payload.WalletAddress, + "Nonce": payload.Nonce, + }).Errorf("Invalid response from identity provider") + u.APIResponse(ctx, http.StatusBadGateway, "error", "Failed to request identity verification", e.Error()) + return + case kycErrors.ErrDatabase: + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", e.Err), + "WalletAddress": payload.WalletAddress, + "Nonce": payload.Nonce, + }).Errorf("Database error during identity verification") + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to request identity verification", nil) + return default: - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "WalletAddress": payload.WalletAddress, + "Nonce": payload.Nonce, + }).Errorf("Failed to request identity verification") u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to request identity verification", nil) return } } u.APIResponse(ctx, http.StatusOK, "success", "Identity verification requested successfully", response) - } // GetIDVerificationStatus controller fetches the status of an identity verification request @@ -712,8 +830,11 @@ func (ctrl *Controller) GetIDVerificationStatus(ctx *gin.Context) { response, err := ctrl.kycService.CheckStatus(ctx, walletAddress) if err != nil { - logger.Errorf("error: %v", err) - if err.Error() == "no verification request found for this wallet address" { + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "WalletAddress": walletAddress, + }).Errorf("Failed to fetch identity verification status") + if fmt.Sprintf("%v", err) == "no verification request found for this wallet address" { u.APIResponse(ctx, http.StatusNotFound, "error", "No verification request found for this wallet address", nil) return } @@ -728,19 +849,22 @@ func (ctrl *Controller) GetIDVerificationStatus(ctx *gin.Context) { func (ctrl *Controller) KYCWebhook(ctx *gin.Context) { payload, err := ctx.GetRawData() if err != nil { - logger.Errorf("Failed to read webhook payload: %v", err) + logger.Errorf("Error: KYCWebhook: Failed to read webhook payload: %v", err) ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}) return } err = ctrl.kycService.HandleWebhook(ctx, payload) if err != nil { - logger.Errorf("error: %v", err) - if err.Error() == "invalid payload" { + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Payload": string(payload), + }).Errorf("Failed to process webhook for kyc") + if fmt.Sprintf("%v", err) == "invalid payload" { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}) return } - if err.Error() == "invalid signature" { + if fmt.Sprintf("%v", err) == "invalid signature" { ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"}) return } diff --git a/controllers/index_test.go b/controllers/index_test.go index fc0b18d4..51f8f2c1 100644 --- a/controllers/index_test.go +++ b/controllers/index_test.go @@ -11,7 +11,6 @@ import ( _ "github.com/mattn/go-sqlite3" "github.com/paycrest/aggregator/config" "github.com/paycrest/aggregator/ent" - "github.com/paycrest/aggregator/services/kyc" db "github.com/paycrest/aggregator/storage" "github.com/paycrest/aggregator/types" "github.com/shopspring/decimal" @@ -152,7 +151,7 @@ func TestIndex(t *testing.T) { }, ) t.Run("with valid details", func(t *testing.T) { - payload := kyc.NewIDVerificationRequest{ + payload := types.VerificationRequest{ WalletAddress: "0xf4c5c4deDde7A86b25E7430796441e209e23eBFB", Signature: "b1dcfa6beba6c93e5abd38c23890a1ff2e553721c5c379a80b66a2ad74b3755f543cd8e7d8fb064ae4fdeeba93302c156bd012e390c2321a763eddaa12e5ab5d1c", Nonce: "e08511abb6087c47", @@ -187,7 +186,7 @@ func TestIndex(t *testing.T) { }) t.Run("with an already used signature", func(t *testing.T) { - payload := kyc.NewIDVerificationRequest{ + payload := types.VerificationRequest{ Signature: "b1dcfa6beba6c93e5abd38c23890a1ff2e553721c5c379a80b66a2ad74b3755f543cd8e7d8fb064ae4fdeeba93302c156bd012e390c2321a763eddaa12e5ab5d1c", WalletAddress: "0xf4c5c4deDde7A86b25E7430796441e209e23eBFB", Nonce: "e08511abb6087c47", @@ -208,7 +207,7 @@ func TestIndex(t *testing.T) { }) t.Run("with a different signature for same wallet address with validity duration", func(t *testing.T) { - payload := kyc.NewIDVerificationRequest{ + payload := types.VerificationRequest{ Signature: "dea3406fa45aa364283e1704b3a8c3b70973a25c262540b71e857efe25e8582b23f98b969cebe320dd2851e5ea36c781253edf7e7d1cd5fe6be704f5709f76df1b", WalletAddress: "0xf4c5c4deDde7A86b25E7430796441e209e23eBFB", Nonce: "8c400162fbfe0527", @@ -228,7 +227,7 @@ func TestIndex(t *testing.T) { }) t.Run("with invalid signature", func(t *testing.T) { - payload := kyc.NewIDVerificationRequest{ + payload := types.VerificationRequest{ Signature: "invalid_signature", WalletAddress: "0xf4c5c4deDde7A86b25E7430796441e209e23eBFB", Nonce: "e08511abb6087c47", diff --git a/controllers/provider/provider.go b/controllers/provider/provider.go index c3fef7d3..a54cb6e5 100644 --- a/controllers/provider/provider.go +++ b/controllers/provider/provider.go @@ -350,7 +350,12 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { // Parse the order payload if err := ctx.ShouldBindJSON(&payload); err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "ValidationError": payload.ValidationError, + "ValidationStatus": payload.ValidationStatus, + }).Errorf("Failed to bind payload to Json for TXID %v", payload.TxID) u.APIResponse(ctx, http.StatusBadRequest, "error", "Failed to validate payload", u.GetErrorData(err)) return @@ -366,7 +371,10 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { // Parse the Order ID string into a UUID orderID, err := uuid.Parse(ctx.Param("id")) if err != nil { - logger.Errorf("error parsing order ID: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + }).Errorf("Error parsing order ID: %v", err) u.APIResponse(ctx, http.StatusBadRequest, "error", "Invalid Order ID", nil) return } @@ -400,7 +408,10 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { SetPsp(payload.PSP). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + }).Errorf("Failed to create lock order fulfillment: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -415,12 +426,20 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { }). Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to fetch lock order fulfillment: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } } else { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to fetch lock order fulfillment when order is found: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -436,7 +455,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { SetValidationStatus(lockorderfulfillment.ValidationStatusSuccess). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to update lock order fulfillment: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -450,7 +473,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { }). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to create transaction log: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -460,7 +487,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { AddTransactions(transactionLog). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to update lock order status: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -474,7 +505,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { err = orderService.NewOrderEVM().SettleOrder(ctx, nil, orderID) } if err != nil { - logger.Errorf("FulfillOrder.SettleOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to settle order: %v", err) } }() @@ -484,7 +519,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { SetValidationError(payload.ValidationError). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to update lock order fulfillment: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -493,7 +532,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { SetStatus(lockpaymentorder.StatusFulfilled). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to update lock order status: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -508,7 +551,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { }). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to create transaction log: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -518,7 +565,11 @@ func (ctrl *ProviderController) FulfillOrder(ctx *gin.Context) { AddTransactions(transactionLog). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Trx Id": payload.TxID, + "Network": fulfillment.Edges.Order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to update lock order status: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to update lock order status", nil) return } @@ -533,7 +584,10 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { // Parse the order payload if err := ctx.ShouldBindJSON(&payload); err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Reason": payload.Reason, + }).Errorf("Failed to validate payload: %v", err) u.APIResponse(ctx, http.StatusBadRequest, "error", "Failed to validate payload", u.GetErrorData(err)) return @@ -550,7 +604,11 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { // Parse the Order ID string into a UUID orderID, err := uuid.Parse(ctx.Param("id")) if err != nil { - logger.Errorf("error parsing order ID: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Reason": payload.Reason, + "Order ID": orderID.String(), + }).Errorf("Error parsing order ID: %v", err) u.APIResponse(ctx, http.StatusBadRequest, "error", "Invalid Order ID", nil) return } @@ -571,7 +629,11 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { }). Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Reason": payload.Reason, + "Order ID": orderID.String(), + }).Errorf("Failed to fetch lock payment order: %v", err) u.APIResponse(ctx, http.StatusNotFound, "error", "Could not find payment order", nil) return } @@ -599,7 +661,9 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { // Extract the id from the data (assuming format "providerID:token:rate:minAmount:maxAmount") parts := strings.Split(providerData, ":") if len(parts) != 5 { - logger.Errorf("invalid provider data format: %s", providerData) + logger.WithFields(logger.Fields{ + "Provider Data": providerData, + }).Error("Invalid provider data format") continue // Skip this entry due to invalid format } @@ -608,13 +672,19 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { placeholder := "DELETED_PROVIDER" // Define a placeholder value _, err := storage.RedisClient.LSet(ctx, redisKey, int64(index), placeholder).Result() if err != nil { - logger.Errorf("failed to set placeholder at index %d: %v", index, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Index": index, + }).Errorf("Failed to set placeholder at index %d: %v", index, err) } // Remove all occurences of the placeholder from the list _, err = storage.RedisClient.LRem(ctx, redisKey, 0, placeholder).Result() if err != nil { - logger.Errorf("failed to remove placeholder from circular queue: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Placeholder": placeholder, + }).Errorf("Failed to remove placeholder from circular queue: %v", err) } break @@ -637,7 +707,11 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { SetCancellationCount(cancellationCount). Save(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Reason": payload.Reason, + "Order ID": orderID.String(), + }).Errorf("Failed to update lock order status: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to cancel order", nil) return } @@ -656,7 +730,12 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { err = orderService.NewOrderEVM().RefundOrder(ctx, nil, order.Edges.Token.Edges.Network, order.GatewayID) } if err != nil { - logger.Errorf("CancelOrder.RefundOrder(%v): %v", orderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Reason": "CancelOrder.RefundOrder", + "Order ID": orderID.String(), + "Network": order.Edges.Token.Edges.Network.Identifier, + }).Errorf("Failed to refund order: %v", err) } }() } @@ -665,7 +744,11 @@ func (ctrl *ProviderController) CancelOrder(ctx *gin.Context) { orderKey := fmt.Sprintf("order_exclude_list_%s", orderID) _, err = storage.RedisClient.RPush(ctx, orderKey, provider.ID).Result() if err != nil { - logger.Errorf("error pushing provider %s to order %s exclude_list on Redis: %v", provider.ID, orderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Order ID": orderID.String(), + }).Errorf("Failed to push provider %s to order %s exclude_list on Redis: %v", provider.ID, orderID, err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to decline order request", nil) return } @@ -690,7 +773,9 @@ func (ctrl *ProviderController) GetMarketRate(ctx *gin.Context) { u.APIResponse(ctx, http.StatusBadRequest, "error", fmt.Sprintf("Token %s is not supported", strings.ToUpper(ctx.Param("token"))), nil) return } - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to get market rate: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to get market rate", nil) return } @@ -703,7 +788,11 @@ func (ctrl *ProviderController) GetMarketRate(ctx *gin.Context) { ). Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": tokenObj.Symbol, + "Fiat": ctx.Param("fiat"), + }).Errorf("Failed to get market rate: %v", err) u.APIResponse(ctx, http.StatusBadRequest, "error", fmt.Sprintf("Fiat currency %s is not supported", strings.ToUpper(ctx.Param("fiat"))), nil) return } @@ -750,7 +839,11 @@ func (ctrl *ProviderController) Stats(ctx *gin.Context) { Where(fiatcurrency.CodeEQ(currency)). Exist(ctx) if err != nil { - logger.Errorf("error checking provider currency: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Currency": currency, + }).Errorf("Failed to check provider currency: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to check currency", nil) return } @@ -789,32 +882,86 @@ func (ctrl *ProviderController) Stats(ctx *gin.Context) { lockpaymentorder.InstitutionIn(institutionCodes...), ) - var v []struct { + // Get USD volume + var usdVolume []struct { Sum decimal.Decimal } - err = query. + Where(lockpaymentorder.HasTokenWith(token.BaseCurrencyEQ("USD"))). Aggregate( ent.Sum(lockpaymentorder.FieldAmount), ). - Scan(ctx, &v) + Scan(ctx, &usdVolume) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Currency": currency, + }).Errorf("Failed to fetch provider stats: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch provider stats", nil) return } - settledOrders, err := query. - All(ctx) + // Get local stablecoin volume + var localStablecoinVolume []struct { + Sum decimal.Decimal + } + err = query. + Where( + lockpaymentorder.HasTokenWith(token.BaseCurrencyEQ(currency)), + lockpaymentorder.HasTokenWith(token.BaseCurrencyNEQ("USD")), + ). + Aggregate( + ent.Sum(lockpaymentorder.FieldAmount), + ). + Scan(ctx, &localStablecoinVolume) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Currency": currency, + }).Errorf("Failed to fetch provider stats: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch provider stats", nil) return } + if localStablecoinVolume[0].Sum.GreaterThan(decimal.NewFromInt(0)) { + // Divide local stablecoin volume by market rate of the currency + fiatCurrency, err := storage.Client.FiatCurrency. + Query(). + Where(fiatcurrency.CodeEQ(currency)). + Only(ctx) + if err != nil { + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Currency": currency, + }).Errorf("Failed to fetch provider fiat currency: %v", err) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch provider stats", nil) + return + } + localStablecoinVolume[0].Sum = localStablecoinVolume[0].Sum.Div(fiatCurrency.MarketRate) + } var totalFiatVolume decimal.Decimal + settledOrders, err := storage.Client.LockPaymentOrder. + Query(). + Where( + lockpaymentorder.HasProviderWith(providerprofile.IDEQ(provider.ID)), + lockpaymentorder.StatusEQ(lockpaymentorder.StatusSettled), + lockpaymentorder.InstitutionIn(institutionCodes...), + ). + All(ctx) + if err != nil { + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Currency": currency, + }).Errorf("Failed to fetch settled orders: %v", err) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch provider stats", nil) + return + } for _, order := range settledOrders { - totalFiatVolume = totalFiatVolume.Add(order.Amount.Mul(order.Rate).RoundBank(0)) + totalFiatVolume = totalFiatVolume.Add(order.Amount.Mul(order.Rate).RoundBank(2)) } count, err := storage.Client.LockPaymentOrder. @@ -825,7 +972,11 @@ func (ctrl *ProviderController) Stats(ctx *gin.Context) { ). Count(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Currency": currency, + }).Errorf("Failed to fetch provider counts with institution codes: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch provider stats", nil) return } @@ -833,7 +984,7 @@ func (ctrl *ProviderController) Stats(ctx *gin.Context) { u.APIResponse(ctx, http.StatusOK, "success", "Provider stats fetched successfully", &types.ProviderStatsResponse{ TotalOrders: count, TotalFiatVolume: totalFiatVolume, - TotalCryptoVolume: v[0].Sum, + TotalCryptoVolume: usdVolume[0].Sum.Add(localStablecoinVolume[0].Sum), }) } @@ -853,7 +1004,9 @@ func (ctrl *ProviderController) NodeInfo(ctx *gin.Context) { WithCurrencies(). Only(ctx) if err != nil { - logger.Errorf("failed to fetch provider: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to fetch provider: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch node info", nil) return } @@ -863,14 +1016,20 @@ func (ctrl *ProviderController) NodeInfo(ctx *gin.Context) { Build().GET("/health"). Send() if err != nil { - logger.Errorf("failed to fetch node info: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Provider": provider.ID, + "Host": provider.HostIdentifier, + }).Errorf("Failed to fetch node info: %v", err) u.APIResponse(ctx, http.StatusServiceUnavailable, "error", "Failed to fetch node info", nil) return } data, err := u.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("failed to parse node info: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("failed to parse node info: %v", err) u.APIResponse(ctx, http.StatusServiceUnavailable, "error", "Failed to fetch node info", nil) return } @@ -914,7 +1073,10 @@ func (ctrl *ProviderController) GetLockPaymentOrderByID(ctx *gin.Context) { // Convert order ID to UUID id, err := uuid.Parse(orderID) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Order ID": orderID, + }).Errorf("Failed to parse order ID: %v", err) u.APIResponse(ctx, http.StatusBadRequest, "error", "Invalid order ID", nil) return @@ -943,7 +1105,10 @@ func (ctrl *ProviderController) GetLockPaymentOrderByID(ctx *gin.Context) { Only(ctx) if err != nil { - logger.Errorf("error: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Order ID": orderID, + }).Errorf("Failed to fetch locked payment order: %v", err) u.APIResponse(ctx, http.StatusNotFound, "error", "Payment order not found", nil) return diff --git a/controllers/sender/sender.go b/controllers/sender/sender.go index 2461596e..51acd4f2 100644 --- a/controllers/sender/sender.go +++ b/controllers/sender/sender.go @@ -11,7 +11,6 @@ import ( "github.com/paycrest/aggregator/config" "github.com/paycrest/aggregator/ent" "github.com/paycrest/aggregator/storage" - "github.com/paycrest/aggregator/utils" "github.com/paycrest/aggregator/ent/fiatcurrency" "github.com/paycrest/aggregator/ent/institution" @@ -282,7 +281,7 @@ func (ctrl *SenderController) InitiatePaymentOrder(ctx *gin.Context) { if orderToken.Edges.Provider.VisibilityMode == providerprofile.VisibilityModePrivate { normalizedAmount := payload.Amount if strings.EqualFold(token.BaseCurrency, institutionObj.Edges.FiatCurrency.Code) && token.BaseCurrency != "USD" { - rateResponse, err := utils.GetTokenRateFromQueue("USDT", normalizedAmount, institutionObj.Edges.FiatCurrency.Code, institutionObj.Edges.FiatCurrency.MarketRate) + rateResponse, err := u.GetTokenRateFromQueue("USDT", normalizedAmount, institutionObj.Edges.FiatCurrency.Code, institutionObj.Edges.FiatCurrency.MarketRate) if err != nil { logger.Errorf("InitiatePaymentOrder.GetTokenRateFromQueue: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to initiate payment order", nil) @@ -415,6 +414,7 @@ func (ctrl *SenderController) InitiatePaymentOrder(ctx *gin.Context) { SetAccountName(payload.Recipient.AccountName). SetProviderID(payload.Recipient.ProviderID). SetMemo(payload.Recipient.Memo). + SetMetadata(payload.Recipient.Metadata). SetPaymentOrder(paymentOrder). Save(ctx) if err != nil { @@ -735,13 +735,18 @@ func (ctrl *SenderController) Stats(ctx *gin.Context) { // Aggregate sender stats from db + // Get USD volume var w []struct { Sum decimal.Decimal SumFieldSenderFee decimal.Decimal } err := storage.Client.PaymentOrder. Query(). - Where(paymentorder.HasSenderProfileWith(senderprofile.IDEQ(sender.ID)), paymentorder.StatusEQ(paymentorder.StatusSettled)). + Where( + paymentorder.HasSenderProfileWith(senderprofile.IDEQ(sender.ID)), + paymentorder.HasTokenWith(tokenEnt.BaseCurrencyEQ("USD")), + paymentorder.StatusEQ(paymentorder.StatusSettled), + ). Aggregate( ent.Sum(paymentorder.FieldAmount), ent.As(ent.Sum(paymentorder.FieldSenderFee), "SumFieldSenderFee"), @@ -753,16 +758,49 @@ func (ctrl *SenderController) Stats(ctx *gin.Context) { return } - var v []struct { - Count int + // Get local stablecoin volume + paymentOrders, err := storage.Client.PaymentOrder. + Query(). + Where( + paymentorder.HasSenderProfileWith(senderprofile.IDEQ(sender.ID)), + paymentorder.HasTokenWith(tokenEnt.BaseCurrencyNEQ("USD")), + paymentorder.StatusEQ(paymentorder.StatusSettled), + ). + WithRecipient(). + All(ctx) + if err != nil { + logger.Errorf("error: %v", err) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch sender stats", nil) + return + } + + var localStablecoinSum decimal.Decimal + var localStablecoinSenderFee decimal.Decimal + + // Convert local stablecoin volume to USD + for _, paymentOrder := range paymentOrders { + institution, err := u.GetInstitutionByCode(ctx, paymentOrder.Edges.Recipient.Institution, false) + if err != nil { + logger.Errorf("error: %v", err) + u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch sender stats", nil) + return + } + + paymentOrder.Amount = paymentOrder.Amount.Div(institution.Edges.FiatCurrency.MarketRate) + if paymentOrder.SenderFee.GreaterThan(decimal.Zero) { + paymentOrder.SenderFee = paymentOrder.SenderFee.Div(institution.Edges.FiatCurrency.MarketRate) + } + + localStablecoinSum = localStablecoinSum.Add(paymentOrder.Amount) + localStablecoinSenderFee = localStablecoinSenderFee.Add(paymentOrder.SenderFee) } - err = storage.Client.PaymentOrder. + + count, err := storage.Client.PaymentOrder. Query(). - Where(paymentorder.HasSenderProfileWith(senderprofile.IDEQ(sender.ID))). - Aggregate( - ent.Count(), + Where( + paymentorder.HasSenderProfileWith(senderprofile.IDEQ(sender.ID)), ). - Scan(ctx, &v) + Count(ctx) if err != nil { logger.Errorf("error: %v", err) u.APIResponse(ctx, http.StatusInternalServerError, "error", "Failed to fetch sender stats", nil) @@ -770,8 +808,8 @@ func (ctrl *SenderController) Stats(ctx *gin.Context) { } u.APIResponse(ctx, http.StatusOK, "success", "Sender stats retrieved successfully", types.SenderStatsResponse{ - TotalOrders: v[0].Count, - TotalOrderVolume: w[0].Sum, - TotalFeeEarnings: w[0].SumFieldSenderFee, + TotalOrders: count, + TotalOrderVolume: w[0].Sum.Add(localStablecoinSum), + TotalFeeEarnings: w[0].SumFieldSenderFee.Add(localStablecoinSenderFee), }) } diff --git a/controllers/sender/sender_test.go b/controllers/sender/sender_test.go index 3d7a21ca..95279ab2 100644 --- a/controllers/sender/sender_test.go +++ b/controllers/sender/sender_test.go @@ -111,7 +111,7 @@ func TestSender(t *testing.T) { // Set up test database client client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1") - defer client.Close() + defer client.Close() db.Client = client diff --git a/ent/linkedaddress.go b/ent/linkedaddress.go index d588b2ea..68bcf42f 100644 --- a/ent/linkedaddress.go +++ b/ent/linkedaddress.go @@ -3,6 +3,7 @@ package ent import ( + "encoding/json" "fmt" "strings" "time" @@ -32,6 +33,8 @@ type LinkedAddress struct { AccountIdentifier string `json:"account_identifier,omitempty"` // AccountName holds the value of the "account_name" field. AccountName string `json:"account_name,omitempty"` + // Metadata holds the value of the "metadata" field. + Metadata map[string]interface{} `json:"metadata,omitempty"` // OwnerAddress holds the value of the "owner_address" field. OwnerAddress string `json:"owner_address,omitempty"` // LastIndexedBlock holds the value of the "last_indexed_block" field. @@ -68,7 +71,7 @@ func (*LinkedAddress) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case linkedaddress.FieldSalt: + case linkedaddress.FieldSalt, linkedaddress.FieldMetadata: values[i] = new([]byte) case linkedaddress.FieldID, linkedaddress.FieldLastIndexedBlock: values[i] = new(sql.NullInt64) @@ -141,6 +144,14 @@ func (la *LinkedAddress) assignValues(columns []string, values []any) error { } else if value.Valid { la.AccountName = value.String } + case linkedaddress.FieldMetadata: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field metadata", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &la.Metadata); err != nil { + return fmt.Errorf("unmarshal field metadata: %w", err) + } + } case linkedaddress.FieldOwnerAddress: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field owner_address", values[i]) @@ -228,6 +239,9 @@ func (la *LinkedAddress) String() string { builder.WriteString("account_name=") builder.WriteString(la.AccountName) builder.WriteString(", ") + builder.WriteString("metadata=") + builder.WriteString(fmt.Sprintf("%v", la.Metadata)) + builder.WriteString(", ") builder.WriteString("owner_address=") builder.WriteString(la.OwnerAddress) builder.WriteString(", ") diff --git a/ent/linkedaddress/linkedaddress.go b/ent/linkedaddress/linkedaddress.go index 401f1e11..4ce915f9 100644 --- a/ent/linkedaddress/linkedaddress.go +++ b/ent/linkedaddress/linkedaddress.go @@ -28,6 +28,8 @@ const ( FieldAccountIdentifier = "account_identifier" // FieldAccountName holds the string denoting the account_name field in the database. FieldAccountName = "account_name" + // FieldMetadata holds the string denoting the metadata field in the database. + FieldMetadata = "metadata" // FieldOwnerAddress holds the string denoting the owner_address field in the database. FieldOwnerAddress = "owner_address" // FieldLastIndexedBlock holds the string denoting the last_indexed_block field in the database. @@ -57,6 +59,7 @@ var Columns = []string{ FieldInstitution, FieldAccountIdentifier, FieldAccountName, + FieldMetadata, FieldOwnerAddress, FieldLastIndexedBlock, FieldTxHash, diff --git a/ent/linkedaddress/where.go b/ent/linkedaddress/where.go index a6f0022b..60acda0a 100644 --- a/ent/linkedaddress/where.go +++ b/ent/linkedaddress/where.go @@ -485,6 +485,16 @@ func AccountNameContainsFold(v string) predicate.LinkedAddress { return predicate.LinkedAddress(sql.FieldContainsFold(FieldAccountName, v)) } +// MetadataIsNil applies the IsNil predicate on the "metadata" field. +func MetadataIsNil() predicate.LinkedAddress { + return predicate.LinkedAddress(sql.FieldIsNull(FieldMetadata)) +} + +// MetadataNotNil applies the NotNil predicate on the "metadata" field. +func MetadataNotNil() predicate.LinkedAddress { + return predicate.LinkedAddress(sql.FieldNotNull(FieldMetadata)) +} + // OwnerAddressEQ applies the EQ predicate on the "owner_address" field. func OwnerAddressEQ(v string) predicate.LinkedAddress { return predicate.LinkedAddress(sql.FieldEQ(FieldOwnerAddress, v)) diff --git a/ent/linkedaddress_create.go b/ent/linkedaddress_create.go index acd94be9..efd8e42e 100644 --- a/ent/linkedaddress_create.go +++ b/ent/linkedaddress_create.go @@ -82,6 +82,12 @@ func (lac *LinkedAddressCreate) SetAccountName(s string) *LinkedAddressCreate { return lac } +// SetMetadata sets the "metadata" field. +func (lac *LinkedAddressCreate) SetMetadata(m map[string]interface{}) *LinkedAddressCreate { + lac.mutation.SetMetadata(m) + return lac +} + // SetOwnerAddress sets the "owner_address" field. func (lac *LinkedAddressCreate) SetOwnerAddress(s string) *LinkedAddressCreate { lac.mutation.SetOwnerAddress(s) @@ -262,6 +268,10 @@ func (lac *LinkedAddressCreate) createSpec() (*LinkedAddress, *sqlgraph.CreateSp _spec.SetField(linkedaddress.FieldAccountName, field.TypeString, value) _node.AccountName = value } + if value, ok := lac.mutation.Metadata(); ok { + _spec.SetField(linkedaddress.FieldMetadata, field.TypeJSON, value) + _node.Metadata = value + } if value, ok := lac.mutation.OwnerAddress(); ok { _spec.SetField(linkedaddress.FieldOwnerAddress, field.TypeString, value) _node.OwnerAddress = value @@ -402,6 +412,24 @@ func (u *LinkedAddressUpsert) UpdateAccountName() *LinkedAddressUpsert { return u } +// SetMetadata sets the "metadata" field. +func (u *LinkedAddressUpsert) SetMetadata(v map[string]interface{}) *LinkedAddressUpsert { + u.Set(linkedaddress.FieldMetadata, v) + return u +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *LinkedAddressUpsert) UpdateMetadata() *LinkedAddressUpsert { + u.SetExcluded(linkedaddress.FieldMetadata) + return u +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *LinkedAddressUpsert) ClearMetadata() *LinkedAddressUpsert { + u.SetNull(linkedaddress.FieldMetadata) + return u +} + // SetOwnerAddress sets the "owner_address" field. func (u *LinkedAddressUpsert) SetOwnerAddress(v string) *LinkedAddressUpsert { u.Set(linkedaddress.FieldOwnerAddress, v) @@ -574,6 +602,27 @@ func (u *LinkedAddressUpsertOne) UpdateAccountName() *LinkedAddressUpsertOne { }) } +// SetMetadata sets the "metadata" field. +func (u *LinkedAddressUpsertOne) SetMetadata(v map[string]interface{}) *LinkedAddressUpsertOne { + return u.Update(func(s *LinkedAddressUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *LinkedAddressUpsertOne) UpdateMetadata() *LinkedAddressUpsertOne { + return u.Update(func(s *LinkedAddressUpsert) { + s.UpdateMetadata() + }) +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *LinkedAddressUpsertOne) ClearMetadata() *LinkedAddressUpsertOne { + return u.Update(func(s *LinkedAddressUpsert) { + s.ClearMetadata() + }) +} + // SetOwnerAddress sets the "owner_address" field. func (u *LinkedAddressUpsertOne) SetOwnerAddress(v string) *LinkedAddressUpsertOne { return u.Update(func(s *LinkedAddressUpsert) { @@ -921,6 +970,27 @@ func (u *LinkedAddressUpsertBulk) UpdateAccountName() *LinkedAddressUpsertBulk { }) } +// SetMetadata sets the "metadata" field. +func (u *LinkedAddressUpsertBulk) SetMetadata(v map[string]interface{}) *LinkedAddressUpsertBulk { + return u.Update(func(s *LinkedAddressUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *LinkedAddressUpsertBulk) UpdateMetadata() *LinkedAddressUpsertBulk { + return u.Update(func(s *LinkedAddressUpsert) { + s.UpdateMetadata() + }) +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *LinkedAddressUpsertBulk) ClearMetadata() *LinkedAddressUpsertBulk { + return u.Update(func(s *LinkedAddressUpsert) { + s.ClearMetadata() + }) +} + // SetOwnerAddress sets the "owner_address" field. func (u *LinkedAddressUpsertBulk) SetOwnerAddress(v string) *LinkedAddressUpsertBulk { return u.Update(func(s *LinkedAddressUpsert) { diff --git a/ent/linkedaddress_update.go b/ent/linkedaddress_update.go index acf8c18f..0cef173d 100644 --- a/ent/linkedaddress_update.go +++ b/ent/linkedaddress_update.go @@ -92,6 +92,18 @@ func (lau *LinkedAddressUpdate) SetNillableAccountName(s *string) *LinkedAddress return lau } +// SetMetadata sets the "metadata" field. +func (lau *LinkedAddressUpdate) SetMetadata(m map[string]interface{}) *LinkedAddressUpdate { + lau.mutation.SetMetadata(m) + return lau +} + +// ClearMetadata clears the value of the "metadata" field. +func (lau *LinkedAddressUpdate) ClearMetadata() *LinkedAddressUpdate { + lau.mutation.ClearMetadata() + return lau +} + // SetOwnerAddress sets the "owner_address" field. func (lau *LinkedAddressUpdate) SetOwnerAddress(s string) *LinkedAddressUpdate { lau.mutation.SetOwnerAddress(s) @@ -267,6 +279,12 @@ func (lau *LinkedAddressUpdate) sqlSave(ctx context.Context) (n int, err error) if value, ok := lau.mutation.AccountName(); ok { _spec.SetField(linkedaddress.FieldAccountName, field.TypeString, value) } + if value, ok := lau.mutation.Metadata(); ok { + _spec.SetField(linkedaddress.FieldMetadata, field.TypeJSON, value) + } + if lau.mutation.MetadataCleared() { + _spec.ClearField(linkedaddress.FieldMetadata, field.TypeJSON) + } if value, ok := lau.mutation.OwnerAddress(); ok { _spec.SetField(linkedaddress.FieldOwnerAddress, field.TypeString, value) } @@ -412,6 +430,18 @@ func (lauo *LinkedAddressUpdateOne) SetNillableAccountName(s *string) *LinkedAdd return lauo } +// SetMetadata sets the "metadata" field. +func (lauo *LinkedAddressUpdateOne) SetMetadata(m map[string]interface{}) *LinkedAddressUpdateOne { + lauo.mutation.SetMetadata(m) + return lauo +} + +// ClearMetadata clears the value of the "metadata" field. +func (lauo *LinkedAddressUpdateOne) ClearMetadata() *LinkedAddressUpdateOne { + lauo.mutation.ClearMetadata() + return lauo +} + // SetOwnerAddress sets the "owner_address" field. func (lauo *LinkedAddressUpdateOne) SetOwnerAddress(s string) *LinkedAddressUpdateOne { lauo.mutation.SetOwnerAddress(s) @@ -617,6 +647,12 @@ func (lauo *LinkedAddressUpdateOne) sqlSave(ctx context.Context) (_node *LinkedA if value, ok := lauo.mutation.AccountName(); ok { _spec.SetField(linkedaddress.FieldAccountName, field.TypeString, value) } + if value, ok := lauo.mutation.Metadata(); ok { + _spec.SetField(linkedaddress.FieldMetadata, field.TypeJSON, value) + } + if lauo.mutation.MetadataCleared() { + _spec.ClearField(linkedaddress.FieldMetadata, field.TypeJSON) + } if value, ok := lauo.mutation.OwnerAddress(); ok { _spec.SetField(linkedaddress.FieldOwnerAddress, field.TypeString, value) } diff --git a/ent/lockpaymentorder.go b/ent/lockpaymentorder.go index 926ca190..30984803 100644 --- a/ent/lockpaymentorder.go +++ b/ent/lockpaymentorder.go @@ -49,6 +49,8 @@ type LockPaymentOrder struct { AccountName string `json:"account_name,omitempty"` // Memo holds the value of the "memo" field. Memo string `json:"memo,omitempty"` + // Metadata holds the value of the "metadata" field. + Metadata map[string]interface{} `json:"metadata,omitempty"` // CancellationCount holds the value of the "cancellation_count" field. CancellationCount int `json:"cancellation_count,omitempty"` // CancellationReasons holds the value of the "cancellation_reasons" field. @@ -135,7 +137,7 @@ func (*LockPaymentOrder) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case lockpaymentorder.FieldCancellationReasons: + case lockpaymentorder.FieldMetadata, lockpaymentorder.FieldCancellationReasons: values[i] = new([]byte) case lockpaymentorder.FieldAmount, lockpaymentorder.FieldRate, lockpaymentorder.FieldOrderPercent: values[i] = new(decimal.Decimal) @@ -252,6 +254,14 @@ func (lpo *LockPaymentOrder) assignValues(columns []string, values []any) error } else if value.Valid { lpo.Memo = value.String } + case lockpaymentorder.FieldMetadata: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field metadata", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &lpo.Metadata); err != nil { + return fmt.Errorf("unmarshal field metadata: %w", err) + } + } case lockpaymentorder.FieldCancellationCount: if value, ok := values[i].(*sql.NullInt64); !ok { return fmt.Errorf("unexpected type %T for field cancellation_count", values[i]) @@ -387,6 +397,9 @@ func (lpo *LockPaymentOrder) String() string { builder.WriteString("memo=") builder.WriteString(lpo.Memo) builder.WriteString(", ") + builder.WriteString("metadata=") + builder.WriteString(fmt.Sprintf("%v", lpo.Metadata)) + builder.WriteString(", ") builder.WriteString("cancellation_count=") builder.WriteString(fmt.Sprintf("%v", lpo.CancellationCount)) builder.WriteString(", ") diff --git a/ent/lockpaymentorder/lockpaymentorder.go b/ent/lockpaymentorder/lockpaymentorder.go index c2a122c7..f5b63fae 100644 --- a/ent/lockpaymentorder/lockpaymentorder.go +++ b/ent/lockpaymentorder/lockpaymentorder.go @@ -42,6 +42,8 @@ const ( FieldAccountName = "account_name" // FieldMemo holds the string denoting the memo field in the database. FieldMemo = "memo" + // FieldMetadata holds the string denoting the metadata field in the database. + FieldMetadata = "metadata" // FieldCancellationCount holds the string denoting the cancellation_count field in the database. FieldCancellationCount = "cancellation_count" // FieldCancellationReasons holds the string denoting the cancellation_reasons field in the database. @@ -111,6 +113,7 @@ var Columns = []string{ FieldAccountIdentifier, FieldAccountName, FieldMemo, + FieldMetadata, FieldCancellationCount, FieldCancellationReasons, } diff --git a/ent/lockpaymentorder/where.go b/ent/lockpaymentorder/where.go index a9ecd324..fd0bf8e3 100644 --- a/ent/lockpaymentorder/where.go +++ b/ent/lockpaymentorder/where.go @@ -792,6 +792,16 @@ func MemoContainsFold(v string) predicate.LockPaymentOrder { return predicate.LockPaymentOrder(sql.FieldContainsFold(FieldMemo, v)) } +// MetadataIsNil applies the IsNil predicate on the "metadata" field. +func MetadataIsNil() predicate.LockPaymentOrder { + return predicate.LockPaymentOrder(sql.FieldIsNull(FieldMetadata)) +} + +// MetadataNotNil applies the NotNil predicate on the "metadata" field. +func MetadataNotNil() predicate.LockPaymentOrder { + return predicate.LockPaymentOrder(sql.FieldNotNull(FieldMetadata)) +} + // CancellationCountEQ applies the EQ predicate on the "cancellation_count" field. func CancellationCountEQ(v int) predicate.LockPaymentOrder { return predicate.LockPaymentOrder(sql.FieldEQ(FieldCancellationCount, v)) diff --git a/ent/lockpaymentorder_create.go b/ent/lockpaymentorder_create.go index 49205947..3ec95609 100644 --- a/ent/lockpaymentorder_create.go +++ b/ent/lockpaymentorder_create.go @@ -148,6 +148,12 @@ func (lpoc *LockPaymentOrderCreate) SetNillableMemo(s *string) *LockPaymentOrder return lpoc } +// SetMetadata sets the "metadata" field. +func (lpoc *LockPaymentOrderCreate) SetMetadata(m map[string]interface{}) *LockPaymentOrderCreate { + lpoc.mutation.SetMetadata(m) + return lpoc +} + // SetCancellationCount sets the "cancellation_count" field. func (lpoc *LockPaymentOrderCreate) SetCancellationCount(i int) *LockPaymentOrderCreate { lpoc.mutation.SetCancellationCount(i) @@ -464,6 +470,10 @@ func (lpoc *LockPaymentOrderCreate) createSpec() (*LockPaymentOrder, *sqlgraph.C _spec.SetField(lockpaymentorder.FieldMemo, field.TypeString, value) _node.Memo = value } + if value, ok := lpoc.mutation.Metadata(); ok { + _spec.SetField(lockpaymentorder.FieldMetadata, field.TypeJSON, value) + _node.Metadata = value + } if value, ok := lpoc.mutation.CancellationCount(); ok { _spec.SetField(lockpaymentorder.FieldCancellationCount, field.TypeInt, value) _node.CancellationCount = value @@ -787,6 +797,24 @@ func (u *LockPaymentOrderUpsert) ClearMemo() *LockPaymentOrderUpsert { return u } +// SetMetadata sets the "metadata" field. +func (u *LockPaymentOrderUpsert) SetMetadata(v map[string]interface{}) *LockPaymentOrderUpsert { + u.Set(lockpaymentorder.FieldMetadata, v) + return u +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *LockPaymentOrderUpsert) UpdateMetadata() *LockPaymentOrderUpsert { + u.SetExcluded(lockpaymentorder.FieldMetadata) + return u +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *LockPaymentOrderUpsert) ClearMetadata() *LockPaymentOrderUpsert { + u.SetNull(lockpaymentorder.FieldMetadata) + return u +} + // SetCancellationCount sets the "cancellation_count" field. func (u *LockPaymentOrderUpsert) SetCancellationCount(v int) *LockPaymentOrderUpsert { u.Set(lockpaymentorder.FieldCancellationCount, v) @@ -1078,6 +1106,27 @@ func (u *LockPaymentOrderUpsertOne) ClearMemo() *LockPaymentOrderUpsertOne { }) } +// SetMetadata sets the "metadata" field. +func (u *LockPaymentOrderUpsertOne) SetMetadata(v map[string]interface{}) *LockPaymentOrderUpsertOne { + return u.Update(func(s *LockPaymentOrderUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *LockPaymentOrderUpsertOne) UpdateMetadata() *LockPaymentOrderUpsertOne { + return u.Update(func(s *LockPaymentOrderUpsert) { + s.UpdateMetadata() + }) +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *LockPaymentOrderUpsertOne) ClearMetadata() *LockPaymentOrderUpsertOne { + return u.Update(func(s *LockPaymentOrderUpsert) { + s.ClearMetadata() + }) +} + // SetCancellationCount sets the "cancellation_count" field. func (u *LockPaymentOrderUpsertOne) SetCancellationCount(v int) *LockPaymentOrderUpsertOne { return u.Update(func(s *LockPaymentOrderUpsert) { @@ -1541,6 +1590,27 @@ func (u *LockPaymentOrderUpsertBulk) ClearMemo() *LockPaymentOrderUpsertBulk { }) } +// SetMetadata sets the "metadata" field. +func (u *LockPaymentOrderUpsertBulk) SetMetadata(v map[string]interface{}) *LockPaymentOrderUpsertBulk { + return u.Update(func(s *LockPaymentOrderUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *LockPaymentOrderUpsertBulk) UpdateMetadata() *LockPaymentOrderUpsertBulk { + return u.Update(func(s *LockPaymentOrderUpsert) { + s.UpdateMetadata() + }) +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *LockPaymentOrderUpsertBulk) ClearMetadata() *LockPaymentOrderUpsertBulk { + return u.Update(func(s *LockPaymentOrderUpsert) { + s.ClearMetadata() + }) +} + // SetCancellationCount sets the "cancellation_count" field. func (u *LockPaymentOrderUpsertBulk) SetCancellationCount(v int) *LockPaymentOrderUpsertBulk { return u.Update(func(s *LockPaymentOrderUpsert) { diff --git a/ent/lockpaymentorder_update.go b/ent/lockpaymentorder_update.go index 7712c63f..816c9c47 100644 --- a/ent/lockpaymentorder_update.go +++ b/ent/lockpaymentorder_update.go @@ -236,6 +236,18 @@ func (lpou *LockPaymentOrderUpdate) ClearMemo() *LockPaymentOrderUpdate { return lpou } +// SetMetadata sets the "metadata" field. +func (lpou *LockPaymentOrderUpdate) SetMetadata(m map[string]interface{}) *LockPaymentOrderUpdate { + lpou.mutation.SetMetadata(m) + return lpou +} + +// ClearMetadata clears the value of the "metadata" field. +func (lpou *LockPaymentOrderUpdate) ClearMetadata() *LockPaymentOrderUpdate { + lpou.mutation.ClearMetadata() + return lpou +} + // SetCancellationCount sets the "cancellation_count" field. func (lpou *LockPaymentOrderUpdate) SetCancellationCount(i int) *LockPaymentOrderUpdate { lpou.mutation.ResetCancellationCount() @@ -533,6 +545,12 @@ func (lpou *LockPaymentOrderUpdate) sqlSave(ctx context.Context) (n int, err err if lpou.mutation.MemoCleared() { _spec.ClearField(lockpaymentorder.FieldMemo, field.TypeString) } + if value, ok := lpou.mutation.Metadata(); ok { + _spec.SetField(lockpaymentorder.FieldMetadata, field.TypeJSON, value) + } + if lpou.mutation.MetadataCleared() { + _spec.ClearField(lockpaymentorder.FieldMetadata, field.TypeJSON) + } if value, ok := lpou.mutation.CancellationCount(); ok { _spec.SetField(lockpaymentorder.FieldCancellationCount, field.TypeInt, value) } @@ -944,6 +962,18 @@ func (lpouo *LockPaymentOrderUpdateOne) ClearMemo() *LockPaymentOrderUpdateOne { return lpouo } +// SetMetadata sets the "metadata" field. +func (lpouo *LockPaymentOrderUpdateOne) SetMetadata(m map[string]interface{}) *LockPaymentOrderUpdateOne { + lpouo.mutation.SetMetadata(m) + return lpouo +} + +// ClearMetadata clears the value of the "metadata" field. +func (lpouo *LockPaymentOrderUpdateOne) ClearMetadata() *LockPaymentOrderUpdateOne { + lpouo.mutation.ClearMetadata() + return lpouo +} + // SetCancellationCount sets the "cancellation_count" field. func (lpouo *LockPaymentOrderUpdateOne) SetCancellationCount(i int) *LockPaymentOrderUpdateOne { lpouo.mutation.ResetCancellationCount() @@ -1271,6 +1301,12 @@ func (lpouo *LockPaymentOrderUpdateOne) sqlSave(ctx context.Context) (_node *Loc if lpouo.mutation.MemoCleared() { _spec.ClearField(lockpaymentorder.FieldMemo, field.TypeString) } + if value, ok := lpouo.mutation.Metadata(); ok { + _spec.SetField(lockpaymentorder.FieldMetadata, field.TypeJSON, value) + } + if lpouo.mutation.MetadataCleared() { + _spec.ClearField(lockpaymentorder.FieldMetadata, field.TypeJSON) + } if value, ok := lpouo.mutation.CancellationCount(); ok { _spec.SetField(lockpaymentorder.FieldCancellationCount, field.TypeInt, value) } diff --git a/ent/migrate/migrations/20250425202927_rate_slippage.sql b/ent/migrate/migrations/20250425202927_rate_slippage.sql new file mode 100644 index 00000000..3cb3c991 --- /dev/null +++ b/ent/migrate/migrations/20250425202927_rate_slippage.sql @@ -0,0 +1,2 @@ +-- Modify "provider_order_tokens" table +ALTER TABLE "provider_order_tokens" ADD COLUMN "rate_slippage" double precision NOT NULL DEFAULT 0; diff --git a/ent/migrate/migrations/20250505033200_tzs_bank_institutions.sql b/ent/migrate/migrations/20250505033200_tzs_bank_institutions.sql new file mode 100644 index 00000000..56b0b0ca --- /dev/null +++ b/ent/migrate/migrations/20250505033200_tzs_bank_institutions.sql @@ -0,0 +1,78 @@ +-- First, check if the TZS fiat currency exists +SELECT EXISTS ( + SELECT 1 FROM "fiat_currencies" + WHERE "code" = 'TZS' +); + +-- If the TZS fiat currency exists, then add the institutions +DO $$ +DECLARE + fiat_currency_id UUID; +BEGIN + -- Get the ID of the TZS fiat currency + SELECT "id" INTO fiat_currency_id + FROM "fiat_currencies" + WHERE "code" = 'TZS'; + + -- Add institutions to the TZS fiat currency + WITH institutions (code, name, type, updated_at, created_at) AS ( + VALUES + ('ACTZTZTZ', 'Access Bank Tanzania Ltd', 'bank', now(), now()), + ('AKCOTZTZ', 'Akiba Commercial Bank PLC', 'bank', now(), now()), + ('AMNNTZTZ', 'Amana Bank Limited', 'bank', now(), now()), + ('AZANTZTZ', 'Azania bank Limited', 'bank', now(), now()), + ('BNKMTZPC', 'bank M Tanzania Public Limited Company', 'bank', now(), now()), + ('EUAFTZTZ', 'Bank of Africa Tanzania Limited', 'bank', now(), now()), + ('BARBTZTZ', 'Bank of Baroda (Tanzania) Ltd', 'bank', now(), now()), + ('BKIDTZTZ', 'Bank Of India (Tanzania) Limited', 'bank', now(), now()), + ('TANZTZTX', 'Bank of Tanzania', 'bank', now(), now()), + ('BARCTZTZ', 'Barclays Bank (Tanzania) Ltd.', 'bank', now(), now()), + ('CNRBTZTZ', 'Canara Bank (Tanzania) Limited', 'bank', now(), now()), + ('CHLMTZTZ', 'China Commercial Bank Limited', 'bank', now(), now()), + ('CITITZTZ', 'Citibank Tanzania Ltd.', 'bank', now(), now()), + ('CBAFTZTZ', 'Commercial Bank of Africa (Tanzania) Limited', 'bank', now(), now()), + ('CBFWTZTZ', 'Covenant Bank For Women (Tanzania) Limited', 'bank', now(), now()), + ('CORUTZTZ', 'CRDB Bank PLC', 'bank', now(), now()), + ('DASUTZTZ', 'DCB Commercial Bank Plc', 'bank', now(), now()), + ('DTKETZTZ', 'Diamond Trust Bank Tanzania Limited', 'bank', now(), now()), + ('ECOCTZTZ', 'Ecobank Tanzania', 'bank', now(), now()), + ('EQBLTZTZ', 'Equity Bank Tanzania Limited', 'bank', now(), now()), + ('EXTNTZTZ', 'Exim Bank (Tanzania) Limited', 'bank', now(), now()), + ('FNMITZTZ', 'FINCA Microfinance Bank Ltd', 'bank', now(), now()), + ('FHTLTZPC', 'First Housing Finance Tanzania Limited', 'bank', now(), now()), + ('FIRNTZTX', 'First National Bank Tanzania Limited', 'bank', now(), now()), + ('GTBITZTZ', 'Guaranty Trust Bank (Tanzania) Limited', 'bank', now(), now()), + ('HABLTZTZ', 'Habib African Bank Ltd.', 'bank', now(), now()), + ('HALOTZPC', 'Halopesa', 'mobile_money', now(), now()), + ('IMBLTZTZ', 'I and M Bank Tanzania Limited', 'bank', now(), now()), + ('BKMYTZTZ', 'International Commercial Bank (Tanzania) Ltd', 'bank', now(), now()), + ('KCBLTZTZ', 'KCB Bank Tanzania Ltd', 'bank', now(), now()), + ('KLMJTZTZ', 'Kilimanjaro Cooperative Bank Limited', 'bank', now(), now()), + ('ADVBTZTZ', 'Letshego Bank (T) Limited', 'bank', now(), now()), + ('MBTLTZTZ', 'Maendeleo Bank Ltd', 'bank', now(), now()), + ('MKCBTZTZ', 'Mkombozi Commercial Bank Ltd', 'bank', now(), now()), + ('MUOBTZTZ', 'Mucoba Bank Plc', 'bank', now(), now()), + ('MWCOTZTZ', 'Mwalimu Commercial Bank Plc', 'bank', now(), now()), + ('MWCBTZTZ', 'Mwanga Community Bank Ltd', 'bank', now(), now()), + ('NLCBTZTX', 'National Bank of Commerce Ltd.', 'bank', now(), now()), + ('NMIBTZTZ', 'National Microfinance Bank Ltd.', 'bank', now(), now()), + ('SFICTZTZ', 'NIC Bank Tanzania Limited', 'bank', now(), now()), + ('SBICTZTX', 'Stanbic Bank Tanzania Ltd', 'bank', now(), now()), + ('SCBLTZTX', 'Standard Chartered Bank Tanzania Ltd.', 'bank', now(), now()), + ('TZADTZTZ', 'Tanzania Agricultural Development Bank', 'bank', now(), now()), + ('PBZATZTZ', 'The People''s Bank of Zanzibar Ltd.', 'bank', now(), now()), + ('TAINTZTZ', 'TIB Corporate Bank Limited', 'bank', now(), now()), + ('TIBDTZPC', 'TIB Development Bank Ltd', 'bank', now(), now()), + ('TAPBTZTZ', 'TPB Bank Company Ltd', 'bank', now(), now()), + ('TRBATZT1', 'Trust Bank (Tanzania) Limited', 'bank', now(), now()), + ('UNILTZTZ', 'UBL Bank (Tanzania) Limited', 'bank', now(), now()), + ('UCCTTZTZ', 'Uchumi Commercial Bank', 'bank', now(), now()), + ('UNAFTZTZ', 'United Bank For Africa (Tanzania) Limited', 'bank', now(), now()), + ('VODATZPC', 'Vodacom', 'mobile_money', now(), now()), + ('YETMTZTZ', 'Yetu Microfinance Bank PLC', 'bank', now(), now()) + ) + INSERT INTO "institutions" ("code", "name", "fiat_currency_institutions", "type", "updated_at", "created_at") + SELECT "code", "name", fiat_currency_id, "type", "updated_at", "created_at" + FROM institutions + ON CONFLICT ("code") DO NOTHING; +END$$; \ No newline at end of file diff --git a/ent/migrate/migrations/20250505033210_ugx_bank_institutions.sql b/ent/migrate/migrations/20250505033210_ugx_bank_institutions.sql new file mode 100644 index 00000000..87675ac2 --- /dev/null +++ b/ent/migrate/migrations/20250505033210_ugx_bank_institutions.sql @@ -0,0 +1,77 @@ +-- First, check if the UGX fiat currency exists +SELECT EXISTS ( + SELECT 1 FROM "fiat_currencies" + WHERE "code" = 'UGX' +); + +-- If the UGX fiat currency exists, then add the institutions +DO $$ +DECLARE + fiat_currency_id UUID; +BEGIN + -- Get the ID of the UGX fiat currency + SELECT "id" INTO fiat_currency_id + FROM "fiat_currencies" + WHERE "code" = 'UGX'; + + -- Add institutions to the UGX fiat currency + WITH institutions (code, name, type, updated_at, created_at) AS ( + VALUES + ('ABCFUGKA', 'ABC Capital Bank Ltd', 'bank', now(), now()), + ('ULTXUGK1', 'Alt Xchange Limited', 'bank', now(), now()), + ('AFRIUGKA', 'Bank of Africa Uganda Ltd', 'bank', now(), now()), + ('BARBUGKA', 'Bank of Baroda (Uganda) Limited', 'bank', now(), now()), + ('BKIDUGKA', 'Bank of India (Uganda) Limited', 'bank', now(), now()), + ('UGBAUGKA', 'Bank of Uganda', 'bank', now(), now()), + ('BARCUGKX', 'Barclays Bank of Uganda Limited', 'bank', now(), now()), + ('BATSUGK1', 'British American Tobacco Uganda Ltd', 'bank', now(), now()), + ('CAIEUGKA', 'Cairo International Bank Limited', 'bank', now(), now()), + ('CERBUGKA', 'Centenary Rural Development Bank Limited', 'bank', now(), now()), + ('CITIUGKA', 'Citibank Uganda Limited', 'bank', now(), now()), + ('COPBUGK1', 'Co-Operative Bank Ltd.', 'bank', now(), now()), + ('CBAFUGKA', 'Commercial Bank of Africa Uganda Limited', 'bank', now(), now()), + ('COBEUGPC', 'Commerzbank - EUR', 'bank', now(), now()), + ('COBGUGPC', 'Commerzbank - GBP', 'bank', now(), now()), + ('COBUUGPC', 'Commerzbank - USD', 'bank', now(), now()), + ('CRANUGKA', 'Crane Bank Ltd', 'bank', now(), now()), + ('CRKSUGK1', 'Crested Stocks And Securities Limited', 'bank', now(), now()), + ('DFCUUGKA', 'DFCU Bank Ltd.', 'bank', now(), now()), + ('DTKEUGKA', 'Diamond Trust Bank Uganda Limited', 'bank', now(), now()), + ('AFDEUGKA', 'East African Development Bank', 'bank', now(), now()), + ('ECOCUGKA', 'Ecobank', 'bank', now(), now()), + ('ENCOUGK1', 'Engiplan Consultants', 'bank', now(), now()), + ('EQBLUGKA', 'Equity Bank Uganda Limited', 'bank', now(), now()), + ('EQSBUGK1', 'Equity Stock Brokers (U) Ltd', 'bank', now(), now()), + ('EXTNUGKA', 'Exim Bank (Uganda) Limited', 'bank', now(), now()), + ('FTBLUGKA', 'Finance Trust Bank Limited', 'bank', now(), now()), + ('FINCAUGPC', 'FINCA Uganda Limited', 'bank', now(), now()), + ('GLTRUGPC', 'Global Trust Bank', 'bank', now(), now()), + ('GTBIUGKA', 'Guaranty Trust Bank Uganda Limited', 'bank', now(), now()), + ('HFINUGKA', 'Housing Finance Bank Limited', 'bank', now(), now()), + ('ICFBUGK1', 'International Credit Bank Ltd.', 'bank', now(), now()), + ('KCBLUGKA', 'KCB Bank Uganda Limited', 'bank', now(), now()), + ('MBRSUGK1', 'Mbea Brokerage Services (U) Limited', 'bank', now(), now()), + ('MCBDUGKB', 'Mercantile Credit Bank Ltd', 'bank', now(), now()), + ('NACOUGKA', 'National Bank of Commerce', 'bank', now(), now()), + ('NINCUGPC', 'NC Bank Uganda Limited', 'bank', now(), now()), + ('NEDTUGPC', 'Nedbank', 'bank', now(), now()), + ('OPUGUGKA', 'Opportunity Bank Uganda Limited', 'bank', now(), now()), + ('ORINUGKA', 'Orient Bank Limited', 'bank', now(), now()), + ('UGPBUGKA', 'Post Bank Uganda Limited', 'bank', now(), now()), + ('PRDEMFPC', 'Pride Microfinance Limited', 'bank', now(), now()), + ('SABMUGT1', 'Sabmiller Uganda', 'bank', now(), now()), + ('SBICUGKX', 'Stanbic Bank Uganda Limited', 'bank', now(), now()), + ('SCBLUGKA', 'Standard Chartered Bank Uganda Limited', 'bank', now(), now()), + ('UIDBUGK1', 'The Uganda Institute of Bankers', 'bank', now(), now()), + ('TOPFUGKA', 'Top Finance Bank Limited', 'bank', now(), now()), + ('TRABUGPC', 'Trans Africa Bank Ltd', 'bank', now(), now()), + ('TROAUGKA', 'Tropical Bank Limited', 'bank', now(), now()), + ('TRBAUGK1', 'Trust Bank (Uganda) Limited', 'bank', now(), now()), + ('USCDUGKA', 'Uganda Securities Exchange', 'bank', now(), now()), + ('UNAFUGKA', 'United Bank for Africa Uganda Limited', 'bank', now(), now()) + ) + INSERT INTO "institutions" ("code", "name", "fiat_currency_institutions", "type", "updated_at", "created_at") + SELECT "code", "name", fiat_currency_id, "type", "updated_at", "created_at" + FROM institutions + ON CONFLICT ("code") DO NOTHING; +END$$; \ No newline at end of file diff --git a/ent/migrate/migrations/20250510231545_order_metadata.sql b/ent/migrate/migrations/20250510231545_order_metadata.sql new file mode 100644 index 00000000..589b8831 --- /dev/null +++ b/ent/migrate/migrations/20250510231545_order_metadata.sql @@ -0,0 +1,8 @@ +-- Modify "linked_addresses" table +ALTER TABLE "linked_addresses" ADD COLUMN "metadata" jsonb NULL; +-- Modify "lock_payment_orders" table +ALTER TABLE "lock_payment_orders" ADD COLUMN "metadata" jsonb NULL; +-- Modify "payment_order_recipients" table +ALTER TABLE "payment_order_recipients" ADD COLUMN "metadata" jsonb NULL; +-- Modify "provider_order_tokens" table +ALTER TABLE "provider_order_tokens" ALTER COLUMN "rate_slippage" DROP DEFAULT; diff --git a/ent/migrate/migrations/atlas.sum b/ent/migrate/migrations/atlas.sum index 984cbbdd..fcbe9818 100644 --- a/ent/migrate/migrations/atlas.sum +++ b/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:6VpQpIFRAvL55EBiPaUw6HNLZlHZ9iKcuBwKz97g6Bs= +h1:mbjYUn+9Ft6WqrWeZurNpG+pt3iyhucaGjJHe2+C+uI= 20240118234246_initial.sql h1:dYuYBqns33WT+3p8VQvbKUP62k3k6w6h8S+FqNqgSvU= 20240130122324_order_from_address.sql h1:mMVI2iBUd1roIYLUqu0d2jZ7+B6exppRN8qqn+aIHx4= 20240202010744_fees_on_order.sql h1:P7ngxZKqDKefBM5vk6M3kbWeMPVwbZ4MZVcLBjEfS34= @@ -49,3 +49,7 @@ h1:6VpQpIFRAvL55EBiPaUw6HNLZlHZ9iKcuBwKz97g6Bs= 20250308153810_multi_currency_data_migrate.sql h1:NTdPUDaCx2HKQa75HGT60EduRQMKDIfrtcN6iL5EcXk= 20250308161319_multi_currency_schema_2.sql h1:q1MY89btb0cmKUwbAOWoAXn8ztx9OGqWrhch8Mc+ARQ= 20250311055352_multi_currency_schema_3.sql h1:1GH1Gy466XwQA12Gr4Nx81x+6MIBZKPDW2WzYrtL+fw= +20250425202927_rate_slippage.sql h1:+I0bp1gHMyiZ+vTvbX+y14Xq/bFTKPmr4LeSUWddYgc= +20250505033200_tzs_bank_institutions.sql h1:kQHdkewxxx63Z5dMQJYsq5LPL1SaeBv8hGv84fpsLCQ= +20250505033210_ugx_bank_institutions.sql h1:8ex3xOuruqqPQJbcR9rUNpPDHU4n8Q3URvUIXQ6oDDE= +20250510231545_order_metadata.sql h1:BrHlYt3LvuYcnsw7b9xfzjg5U4D1RfPy+RusxRDj66k= diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 4f846dac..3fe001b0 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -107,6 +107,7 @@ var ( {Name: "institution", Type: field.TypeString}, {Name: "account_identifier", Type: field.TypeString}, {Name: "account_name", Type: field.TypeString}, + {Name: "metadata", Type: field.TypeJSON, Nullable: true}, {Name: "owner_address", Type: field.TypeString, Unique: true}, {Name: "last_indexed_block", Type: field.TypeInt64, Nullable: true}, {Name: "tx_hash", Type: field.TypeString, Nullable: true, Size: 70}, @@ -120,7 +121,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "linked_addresses_sender_profiles_linked_address", - Columns: []*schema.Column{LinkedAddressesColumns[11]}, + Columns: []*schema.Column{LinkedAddressesColumns[12]}, RefColumns: []*schema.Column{SenderProfilesColumns[0]}, OnDelete: schema.Cascade, }, @@ -167,6 +168,7 @@ var ( {Name: "account_identifier", Type: field.TypeString}, {Name: "account_name", Type: field.TypeString}, {Name: "memo", Type: field.TypeString, Nullable: true}, + {Name: "metadata", Type: field.TypeJSON, Nullable: true}, {Name: "cancellation_count", Type: field.TypeInt, Default: 0}, {Name: "cancellation_reasons", Type: field.TypeJSON}, {Name: "provider_profile_assigned_orders", Type: field.TypeString, Nullable: true}, @@ -181,19 +183,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "lock_payment_orders_provider_profiles_assigned_orders", - Columns: []*schema.Column{LockPaymentOrdersColumns[16]}, + Columns: []*schema.Column{LockPaymentOrdersColumns[17]}, RefColumns: []*schema.Column{ProviderProfilesColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "lock_payment_orders_provision_buckets_lock_payment_orders", - Columns: []*schema.Column{LockPaymentOrdersColumns[17]}, + Columns: []*schema.Column{LockPaymentOrdersColumns[18]}, RefColumns: []*schema.Column{ProvisionBucketsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "lock_payment_orders_tokens_lock_payment_orders", - Columns: []*schema.Column{LockPaymentOrdersColumns[18]}, + Columns: []*schema.Column{LockPaymentOrdersColumns[19]}, RefColumns: []*schema.Column{TokensColumns[0]}, OnDelete: schema.Cascade, }, @@ -202,7 +204,7 @@ var ( { Name: "lockpaymentorder_gateway_id_rate_tx_hash_block_number_institution_account_identifier_account_name_memo_token_lock_payment_orders", Unique: true, - Columns: []*schema.Column{LockPaymentOrdersColumns[3], LockPaymentOrdersColumns[5], LockPaymentOrdersColumns[7], LockPaymentOrdersColumns[9], LockPaymentOrdersColumns[10], LockPaymentOrdersColumns[11], LockPaymentOrdersColumns[12], LockPaymentOrdersColumns[13], LockPaymentOrdersColumns[18]}, + Columns: []*schema.Column{LockPaymentOrdersColumns[3], LockPaymentOrdersColumns[5], LockPaymentOrdersColumns[7], LockPaymentOrdersColumns[9], LockPaymentOrdersColumns[10], LockPaymentOrdersColumns[11], LockPaymentOrdersColumns[12], LockPaymentOrdersColumns[13], LockPaymentOrdersColumns[19]}, }, }, } @@ -295,6 +297,7 @@ var ( {Name: "account_name", Type: field.TypeString}, {Name: "memo", Type: field.TypeString, Nullable: true}, {Name: "provider_id", Type: field.TypeString, Nullable: true}, + {Name: "metadata", Type: field.TypeJSON, Nullable: true}, {Name: "payment_order_recipient", Type: field.TypeUUID, Unique: true}, } // PaymentOrderRecipientsTable holds the schema information for the "payment_order_recipients" table. @@ -305,7 +308,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "payment_order_recipients_payment_orders_recipient", - Columns: []*schema.Column{PaymentOrderRecipientsColumns[6]}, + Columns: []*schema.Column{PaymentOrderRecipientsColumns[7]}, RefColumns: []*schema.Column{PaymentOrdersColumns[0]}, OnDelete: schema.Cascade, }, @@ -321,6 +324,7 @@ var ( {Name: "conversion_rate_type", Type: field.TypeEnum, Enums: []string{"fixed", "floating"}}, {Name: "max_order_amount", Type: field.TypeFloat64}, {Name: "min_order_amount", Type: field.TypeFloat64}, + {Name: "rate_slippage", Type: field.TypeFloat64}, {Name: "address", Type: field.TypeString, Nullable: true}, {Name: "network", Type: field.TypeString, Nullable: true}, {Name: "fiat_currency_provider_order_tokens", Type: field.TypeUUID}, @@ -335,19 +339,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "provider_order_tokens_fiat_currencies_provider_order_tokens", - Columns: []*schema.Column{ProviderOrderTokensColumns[10]}, + Columns: []*schema.Column{ProviderOrderTokensColumns[11]}, RefColumns: []*schema.Column{FiatCurrenciesColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "provider_order_tokens_provider_profiles_order_tokens", - Columns: []*schema.Column{ProviderOrderTokensColumns[11]}, + Columns: []*schema.Column{ProviderOrderTokensColumns[12]}, RefColumns: []*schema.Column{ProviderProfilesColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "provider_order_tokens_tokens_provider_order_tokens", - Columns: []*schema.Column{ProviderOrderTokensColumns[12]}, + Columns: []*schema.Column{ProviderOrderTokensColumns[13]}, RefColumns: []*schema.Column{TokensColumns[0]}, OnDelete: schema.Cascade, }, @@ -356,7 +360,7 @@ var ( { Name: "providerordertoken_provider_profile_order_tokens_token_provider_order_tokens_fiat_currency_provider_order_tokens", Unique: true, - Columns: []*schema.Column{ProviderOrderTokensColumns[11], ProviderOrderTokensColumns[12], ProviderOrderTokensColumns[10]}, + Columns: []*schema.Column{ProviderOrderTokensColumns[12], ProviderOrderTokensColumns[13], ProviderOrderTokensColumns[11]}, }, }, } diff --git a/ent/mutation.go b/ent/mutation.go index a4f82e43..cd7001e0 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -3175,6 +3175,7 @@ type LinkedAddressMutation struct { institution *string account_identifier *string account_name *string + metadata *map[string]interface{} owner_address *string last_indexed_block *int64 addlast_indexed_block *int64 @@ -3538,6 +3539,55 @@ func (m *LinkedAddressMutation) ResetAccountName() { m.account_name = nil } +// SetMetadata sets the "metadata" field. +func (m *LinkedAddressMutation) SetMetadata(value map[string]interface{}) { + m.metadata = &value +} + +// Metadata returns the value of the "metadata" field in the mutation. +func (m *LinkedAddressMutation) Metadata() (r map[string]interface{}, exists bool) { + v := m.metadata + if v == nil { + return + } + return *v, true +} + +// OldMetadata returns the old "metadata" field's value of the LinkedAddress entity. +// If the LinkedAddress object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LinkedAddressMutation) OldMetadata(ctx context.Context) (v map[string]interface{}, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMetadata is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMetadata requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMetadata: %w", err) + } + return oldValue.Metadata, nil +} + +// ClearMetadata clears the value of the "metadata" field. +func (m *LinkedAddressMutation) ClearMetadata() { + m.metadata = nil + m.clearedFields[linkedaddress.FieldMetadata] = struct{}{} +} + +// MetadataCleared returns if the "metadata" field was cleared in this mutation. +func (m *LinkedAddressMutation) MetadataCleared() bool { + _, ok := m.clearedFields[linkedaddress.FieldMetadata] + return ok +} + +// ResetMetadata resets all changes to the "metadata" field. +func (m *LinkedAddressMutation) ResetMetadata() { + m.metadata = nil + delete(m.clearedFields, linkedaddress.FieldMetadata) +} + // SetOwnerAddress sets the "owner_address" field. func (m *LinkedAddressMutation) SetOwnerAddress(s string) { m.owner_address = &s @@ -3781,7 +3831,7 @@ func (m *LinkedAddressMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *LinkedAddressMutation) Fields() []string { - fields := make([]string, 0, 10) + fields := make([]string, 0, 11) if m.created_at != nil { fields = append(fields, linkedaddress.FieldCreatedAt) } @@ -3803,6 +3853,9 @@ func (m *LinkedAddressMutation) Fields() []string { if m.account_name != nil { fields = append(fields, linkedaddress.FieldAccountName) } + if m.metadata != nil { + fields = append(fields, linkedaddress.FieldMetadata) + } if m.owner_address != nil { fields = append(fields, linkedaddress.FieldOwnerAddress) } @@ -3834,6 +3887,8 @@ func (m *LinkedAddressMutation) Field(name string) (ent.Value, bool) { return m.AccountIdentifier() case linkedaddress.FieldAccountName: return m.AccountName() + case linkedaddress.FieldMetadata: + return m.Metadata() case linkedaddress.FieldOwnerAddress: return m.OwnerAddress() case linkedaddress.FieldLastIndexedBlock: @@ -3863,6 +3918,8 @@ func (m *LinkedAddressMutation) OldField(ctx context.Context, name string) (ent. return m.OldAccountIdentifier(ctx) case linkedaddress.FieldAccountName: return m.OldAccountName(ctx) + case linkedaddress.FieldMetadata: + return m.OldMetadata(ctx) case linkedaddress.FieldOwnerAddress: return m.OldOwnerAddress(ctx) case linkedaddress.FieldLastIndexedBlock: @@ -3927,6 +3984,13 @@ func (m *LinkedAddressMutation) SetField(name string, value ent.Value) error { } m.SetAccountName(v) return nil + case linkedaddress.FieldMetadata: + v, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMetadata(v) + return nil case linkedaddress.FieldOwnerAddress: v, ok := value.(string) if !ok { @@ -3993,6 +4057,9 @@ func (m *LinkedAddressMutation) AddField(name string, value ent.Value) error { // mutation. func (m *LinkedAddressMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(linkedaddress.FieldMetadata) { + fields = append(fields, linkedaddress.FieldMetadata) + } if m.FieldCleared(linkedaddress.FieldLastIndexedBlock) { fields = append(fields, linkedaddress.FieldLastIndexedBlock) } @@ -4013,6 +4080,9 @@ func (m *LinkedAddressMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *LinkedAddressMutation) ClearField(name string) error { switch name { + case linkedaddress.FieldMetadata: + m.ClearMetadata() + return nil case linkedaddress.FieldLastIndexedBlock: m.ClearLastIndexedBlock() return nil @@ -4048,6 +4118,9 @@ func (m *LinkedAddressMutation) ResetField(name string) error { case linkedaddress.FieldAccountName: m.ResetAccountName() return nil + case linkedaddress.FieldMetadata: + m.ResetMetadata() + return nil case linkedaddress.FieldOwnerAddress: m.ResetOwnerAddress() return nil @@ -4897,6 +4970,7 @@ type LockPaymentOrderMutation struct { account_identifier *string account_name *string memo *string + metadata *map[string]interface{} cancellation_count *int addcancellation_count *int cancellation_reasons *[]string @@ -5597,6 +5671,55 @@ func (m *LockPaymentOrderMutation) ResetMemo() { delete(m.clearedFields, lockpaymentorder.FieldMemo) } +// SetMetadata sets the "metadata" field. +func (m *LockPaymentOrderMutation) SetMetadata(value map[string]interface{}) { + m.metadata = &value +} + +// Metadata returns the value of the "metadata" field in the mutation. +func (m *LockPaymentOrderMutation) Metadata() (r map[string]interface{}, exists bool) { + v := m.metadata + if v == nil { + return + } + return *v, true +} + +// OldMetadata returns the old "metadata" field's value of the LockPaymentOrder entity. +// If the LockPaymentOrder object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *LockPaymentOrderMutation) OldMetadata(ctx context.Context) (v map[string]interface{}, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMetadata is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMetadata requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMetadata: %w", err) + } + return oldValue.Metadata, nil +} + +// ClearMetadata clears the value of the "metadata" field. +func (m *LockPaymentOrderMutation) ClearMetadata() { + m.metadata = nil + m.clearedFields[lockpaymentorder.FieldMetadata] = struct{}{} +} + +// MetadataCleared returns if the "metadata" field was cleared in this mutation. +func (m *LockPaymentOrderMutation) MetadataCleared() bool { + _, ok := m.clearedFields[lockpaymentorder.FieldMetadata] + return ok +} + +// ResetMetadata resets all changes to the "metadata" field. +func (m *LockPaymentOrderMutation) ResetMetadata() { + m.metadata = nil + delete(m.clearedFields, lockpaymentorder.FieldMetadata) +} + // SetCancellationCount sets the "cancellation_count" field. func (m *LockPaymentOrderMutation) SetCancellationCount(i int) { m.cancellation_count = &i @@ -5963,7 +6086,7 @@ func (m *LockPaymentOrderMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *LockPaymentOrderMutation) Fields() []string { - fields := make([]string, 0, 15) + fields := make([]string, 0, 16) if m.created_at != nil { fields = append(fields, lockpaymentorder.FieldCreatedAt) } @@ -6003,6 +6126,9 @@ func (m *LockPaymentOrderMutation) Fields() []string { if m.memo != nil { fields = append(fields, lockpaymentorder.FieldMemo) } + if m.metadata != nil { + fields = append(fields, lockpaymentorder.FieldMetadata) + } if m.cancellation_count != nil { fields = append(fields, lockpaymentorder.FieldCancellationCount) } @@ -6043,6 +6169,8 @@ func (m *LockPaymentOrderMutation) Field(name string) (ent.Value, bool) { return m.AccountName() case lockpaymentorder.FieldMemo: return m.Memo() + case lockpaymentorder.FieldMetadata: + return m.Metadata() case lockpaymentorder.FieldCancellationCount: return m.CancellationCount() case lockpaymentorder.FieldCancellationReasons: @@ -6082,6 +6210,8 @@ func (m *LockPaymentOrderMutation) OldField(ctx context.Context, name string) (e return m.OldAccountName(ctx) case lockpaymentorder.FieldMemo: return m.OldMemo(ctx) + case lockpaymentorder.FieldMetadata: + return m.OldMetadata(ctx) case lockpaymentorder.FieldCancellationCount: return m.OldCancellationCount(ctx) case lockpaymentorder.FieldCancellationReasons: @@ -6186,6 +6316,13 @@ func (m *LockPaymentOrderMutation) SetField(name string, value ent.Value) error } m.SetMemo(v) return nil + case lockpaymentorder.FieldMetadata: + v, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMetadata(v) + return nil case lockpaymentorder.FieldCancellationCount: v, ok := value.(int) if !ok { @@ -6299,6 +6436,9 @@ func (m *LockPaymentOrderMutation) ClearedFields() []string { if m.FieldCleared(lockpaymentorder.FieldMemo) { fields = append(fields, lockpaymentorder.FieldMemo) } + if m.FieldCleared(lockpaymentorder.FieldMetadata) { + fields = append(fields, lockpaymentorder.FieldMetadata) + } return fields } @@ -6319,6 +6459,9 @@ func (m *LockPaymentOrderMutation) ClearField(name string) error { case lockpaymentorder.FieldMemo: m.ClearMemo() return nil + case lockpaymentorder.FieldMetadata: + m.ClearMetadata() + return nil } return fmt.Errorf("unknown LockPaymentOrder nullable field %s", name) } @@ -6366,6 +6509,9 @@ func (m *LockPaymentOrderMutation) ResetField(name string) error { case lockpaymentorder.FieldMemo: m.ResetMemo() return nil + case lockpaymentorder.FieldMetadata: + m.ResetMetadata() + return nil case lockpaymentorder.FieldCancellationCount: m.ResetCancellationCount() return nil @@ -9835,6 +9981,7 @@ type PaymentOrderRecipientMutation struct { account_name *string memo *string provider_id *string + metadata *map[string]interface{} clearedFields map[string]struct{} payment_order *uuid.UUID clearedpayment_order bool @@ -10147,6 +10294,55 @@ func (m *PaymentOrderRecipientMutation) ResetProviderID() { delete(m.clearedFields, paymentorderrecipient.FieldProviderID) } +// SetMetadata sets the "metadata" field. +func (m *PaymentOrderRecipientMutation) SetMetadata(value map[string]interface{}) { + m.metadata = &value +} + +// Metadata returns the value of the "metadata" field in the mutation. +func (m *PaymentOrderRecipientMutation) Metadata() (r map[string]interface{}, exists bool) { + v := m.metadata + if v == nil { + return + } + return *v, true +} + +// OldMetadata returns the old "metadata" field's value of the PaymentOrderRecipient entity. +// If the PaymentOrderRecipient object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PaymentOrderRecipientMutation) OldMetadata(ctx context.Context) (v map[string]interface{}, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldMetadata is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldMetadata requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldMetadata: %w", err) + } + return oldValue.Metadata, nil +} + +// ClearMetadata clears the value of the "metadata" field. +func (m *PaymentOrderRecipientMutation) ClearMetadata() { + m.metadata = nil + m.clearedFields[paymentorderrecipient.FieldMetadata] = struct{}{} +} + +// MetadataCleared returns if the "metadata" field was cleared in this mutation. +func (m *PaymentOrderRecipientMutation) MetadataCleared() bool { + _, ok := m.clearedFields[paymentorderrecipient.FieldMetadata] + return ok +} + +// ResetMetadata resets all changes to the "metadata" field. +func (m *PaymentOrderRecipientMutation) ResetMetadata() { + m.metadata = nil + delete(m.clearedFields, paymentorderrecipient.FieldMetadata) +} + // SetPaymentOrderID sets the "payment_order" edge to the PaymentOrder entity by id. func (m *PaymentOrderRecipientMutation) SetPaymentOrderID(id uuid.UUID) { m.payment_order = &id @@ -10220,7 +10416,7 @@ func (m *PaymentOrderRecipientMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *PaymentOrderRecipientMutation) Fields() []string { - fields := make([]string, 0, 5) + fields := make([]string, 0, 6) if m.institution != nil { fields = append(fields, paymentorderrecipient.FieldInstitution) } @@ -10236,6 +10432,9 @@ func (m *PaymentOrderRecipientMutation) Fields() []string { if m.provider_id != nil { fields = append(fields, paymentorderrecipient.FieldProviderID) } + if m.metadata != nil { + fields = append(fields, paymentorderrecipient.FieldMetadata) + } return fields } @@ -10254,6 +10453,8 @@ func (m *PaymentOrderRecipientMutation) Field(name string) (ent.Value, bool) { return m.Memo() case paymentorderrecipient.FieldProviderID: return m.ProviderID() + case paymentorderrecipient.FieldMetadata: + return m.Metadata() } return nil, false } @@ -10273,6 +10474,8 @@ func (m *PaymentOrderRecipientMutation) OldField(ctx context.Context, name strin return m.OldMemo(ctx) case paymentorderrecipient.FieldProviderID: return m.OldProviderID(ctx) + case paymentorderrecipient.FieldMetadata: + return m.OldMetadata(ctx) } return nil, fmt.Errorf("unknown PaymentOrderRecipient field %s", name) } @@ -10317,6 +10520,13 @@ func (m *PaymentOrderRecipientMutation) SetField(name string, value ent.Value) e } m.SetProviderID(v) return nil + case paymentorderrecipient.FieldMetadata: + v, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetMetadata(v) + return nil } return fmt.Errorf("unknown PaymentOrderRecipient field %s", name) } @@ -10353,6 +10563,9 @@ func (m *PaymentOrderRecipientMutation) ClearedFields() []string { if m.FieldCleared(paymentorderrecipient.FieldProviderID) { fields = append(fields, paymentorderrecipient.FieldProviderID) } + if m.FieldCleared(paymentorderrecipient.FieldMetadata) { + fields = append(fields, paymentorderrecipient.FieldMetadata) + } return fields } @@ -10373,6 +10586,9 @@ func (m *PaymentOrderRecipientMutation) ClearField(name string) error { case paymentorderrecipient.FieldProviderID: m.ClearProviderID() return nil + case paymentorderrecipient.FieldMetadata: + m.ClearMetadata() + return nil } return fmt.Errorf("unknown PaymentOrderRecipient nullable field %s", name) } @@ -10396,6 +10612,9 @@ func (m *PaymentOrderRecipientMutation) ResetField(name string) error { case paymentorderrecipient.FieldProviderID: m.ResetProviderID() return nil + case paymentorderrecipient.FieldMetadata: + m.ResetMetadata() + return nil } return fmt.Errorf("unknown PaymentOrderRecipient field %s", name) } @@ -10491,6 +10710,8 @@ type ProviderOrderTokenMutation struct { addmax_order_amount *decimal.Decimal min_order_amount *decimal.Decimal addmin_order_amount *decimal.Decimal + rate_slippage *decimal.Decimal + addrate_slippage *decimal.Decimal address *string network *string clearedFields map[string]struct{} @@ -10935,6 +11156,62 @@ func (m *ProviderOrderTokenMutation) ResetMinOrderAmount() { m.addmin_order_amount = nil } +// SetRateSlippage sets the "rate_slippage" field. +func (m *ProviderOrderTokenMutation) SetRateSlippage(d decimal.Decimal) { + m.rate_slippage = &d + m.addrate_slippage = nil +} + +// RateSlippage returns the value of the "rate_slippage" field in the mutation. +func (m *ProviderOrderTokenMutation) RateSlippage() (r decimal.Decimal, exists bool) { + v := m.rate_slippage + if v == nil { + return + } + return *v, true +} + +// OldRateSlippage returns the old "rate_slippage" field's value of the ProviderOrderToken entity. +// If the ProviderOrderToken object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ProviderOrderTokenMutation) OldRateSlippage(ctx context.Context) (v decimal.Decimal, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRateSlippage is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRateSlippage requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRateSlippage: %w", err) + } + return oldValue.RateSlippage, nil +} + +// AddRateSlippage adds d to the "rate_slippage" field. +func (m *ProviderOrderTokenMutation) AddRateSlippage(d decimal.Decimal) { + if m.addrate_slippage != nil { + *m.addrate_slippage = m.addrate_slippage.Add(d) + } else { + m.addrate_slippage = &d + } +} + +// AddedRateSlippage returns the value that was added to the "rate_slippage" field in this mutation. +func (m *ProviderOrderTokenMutation) AddedRateSlippage() (r decimal.Decimal, exists bool) { + v := m.addrate_slippage + if v == nil { + return + } + return *v, true +} + +// ResetRateSlippage resets all changes to the "rate_slippage" field. +func (m *ProviderOrderTokenMutation) ResetRateSlippage() { + m.rate_slippage = nil + m.addrate_slippage = nil +} + // SetAddress sets the "address" field. func (m *ProviderOrderTokenMutation) SetAddress(s string) { m.address = &s @@ -11184,7 +11461,7 @@ func (m *ProviderOrderTokenMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ProviderOrderTokenMutation) Fields() []string { - fields := make([]string, 0, 9) + fields := make([]string, 0, 10) if m.created_at != nil { fields = append(fields, providerordertoken.FieldCreatedAt) } @@ -11206,6 +11483,9 @@ func (m *ProviderOrderTokenMutation) Fields() []string { if m.min_order_amount != nil { fields = append(fields, providerordertoken.FieldMinOrderAmount) } + if m.rate_slippage != nil { + fields = append(fields, providerordertoken.FieldRateSlippage) + } if m.address != nil { fields = append(fields, providerordertoken.FieldAddress) } @@ -11234,6 +11514,8 @@ func (m *ProviderOrderTokenMutation) Field(name string) (ent.Value, bool) { return m.MaxOrderAmount() case providerordertoken.FieldMinOrderAmount: return m.MinOrderAmount() + case providerordertoken.FieldRateSlippage: + return m.RateSlippage() case providerordertoken.FieldAddress: return m.Address() case providerordertoken.FieldNetwork: @@ -11261,6 +11543,8 @@ func (m *ProviderOrderTokenMutation) OldField(ctx context.Context, name string) return m.OldMaxOrderAmount(ctx) case providerordertoken.FieldMinOrderAmount: return m.OldMinOrderAmount(ctx) + case providerordertoken.FieldRateSlippage: + return m.OldRateSlippage(ctx) case providerordertoken.FieldAddress: return m.OldAddress(ctx) case providerordertoken.FieldNetwork: @@ -11323,6 +11607,13 @@ func (m *ProviderOrderTokenMutation) SetField(name string, value ent.Value) erro } m.SetMinOrderAmount(v) return nil + case providerordertoken.FieldRateSlippage: + v, ok := value.(decimal.Decimal) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRateSlippage(v) + return nil case providerordertoken.FieldAddress: v, ok := value.(string) if !ok { @@ -11357,6 +11648,9 @@ func (m *ProviderOrderTokenMutation) AddedFields() []string { if m.addmin_order_amount != nil { fields = append(fields, providerordertoken.FieldMinOrderAmount) } + if m.addrate_slippage != nil { + fields = append(fields, providerordertoken.FieldRateSlippage) + } return fields } @@ -11373,6 +11667,8 @@ func (m *ProviderOrderTokenMutation) AddedField(name string) (ent.Value, bool) { return m.AddedMaxOrderAmount() case providerordertoken.FieldMinOrderAmount: return m.AddedMinOrderAmount() + case providerordertoken.FieldRateSlippage: + return m.AddedRateSlippage() } return nil, false } @@ -11410,6 +11706,13 @@ func (m *ProviderOrderTokenMutation) AddField(name string, value ent.Value) erro } m.AddMinOrderAmount(v) return nil + case providerordertoken.FieldRateSlippage: + v, ok := value.(decimal.Decimal) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddRateSlippage(v) + return nil } return fmt.Errorf("unknown ProviderOrderToken numeric field %s", name) } @@ -11473,6 +11776,9 @@ func (m *ProviderOrderTokenMutation) ResetField(name string) error { case providerordertoken.FieldMinOrderAmount: m.ResetMinOrderAmount() return nil + case providerordertoken.FieldRateSlippage: + m.ResetRateSlippage() + return nil case providerordertoken.FieldAddress: m.ResetAddress() return nil diff --git a/ent/paymentorderrecipient.go b/ent/paymentorderrecipient.go index 4fcf3110..6fd9d81c 100644 --- a/ent/paymentorderrecipient.go +++ b/ent/paymentorderrecipient.go @@ -3,6 +3,7 @@ package ent import ( + "encoding/json" "fmt" "strings" @@ -28,6 +29,8 @@ type PaymentOrderRecipient struct { Memo string `json:"memo,omitempty"` // ProviderID holds the value of the "provider_id" field. ProviderID string `json:"provider_id,omitempty"` + // Metadata holds the value of the "metadata" field. + Metadata map[string]interface{} `json:"metadata,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the PaymentOrderRecipientQuery when eager-loading is set. Edges PaymentOrderRecipientEdges `json:"edges"` @@ -60,6 +63,8 @@ func (*PaymentOrderRecipient) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { + case paymentorderrecipient.FieldMetadata: + values[i] = new([]byte) case paymentorderrecipient.FieldID: values[i] = new(sql.NullInt64) case paymentorderrecipient.FieldInstitution, paymentorderrecipient.FieldAccountIdentifier, paymentorderrecipient.FieldAccountName, paymentorderrecipient.FieldMemo, paymentorderrecipient.FieldProviderID: @@ -117,6 +122,14 @@ func (por *PaymentOrderRecipient) assignValues(columns []string, values []any) e } else if value.Valid { por.ProviderID = value.String } + case paymentorderrecipient.FieldMetadata: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field metadata", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &por.Metadata); err != nil { + return fmt.Errorf("unmarshal field metadata: %w", err) + } + } case paymentorderrecipient.ForeignKeys[0]: if value, ok := values[i].(*sql.NullScanner); !ok { return fmt.Errorf("unexpected type %T for field payment_order_recipient", values[i]) @@ -179,6 +192,9 @@ func (por *PaymentOrderRecipient) String() string { builder.WriteString(", ") builder.WriteString("provider_id=") builder.WriteString(por.ProviderID) + builder.WriteString(", ") + builder.WriteString("metadata=") + builder.WriteString(fmt.Sprintf("%v", por.Metadata)) builder.WriteByte(')') return builder.String() } diff --git a/ent/paymentorderrecipient/paymentorderrecipient.go b/ent/paymentorderrecipient/paymentorderrecipient.go index 1dac9d41..e470d711 100644 --- a/ent/paymentorderrecipient/paymentorderrecipient.go +++ b/ent/paymentorderrecipient/paymentorderrecipient.go @@ -22,6 +22,8 @@ const ( FieldMemo = "memo" // FieldProviderID holds the string denoting the provider_id field in the database. FieldProviderID = "provider_id" + // FieldMetadata holds the string denoting the metadata field in the database. + FieldMetadata = "metadata" // EdgePaymentOrder holds the string denoting the payment_order edge name in mutations. EdgePaymentOrder = "payment_order" // Table holds the table name of the paymentorderrecipient in the database. @@ -43,6 +45,7 @@ var Columns = []string{ FieldAccountName, FieldMemo, FieldProviderID, + FieldMetadata, } // ForeignKeys holds the SQL foreign-keys that are owned by the "payment_order_recipients" diff --git a/ent/paymentorderrecipient/where.go b/ent/paymentorderrecipient/where.go index 9414c0bb..0bf05767 100644 --- a/ent/paymentorderrecipient/where.go +++ b/ent/paymentorderrecipient/where.go @@ -423,6 +423,16 @@ func ProviderIDContainsFold(v string) predicate.PaymentOrderRecipient { return predicate.PaymentOrderRecipient(sql.FieldContainsFold(FieldProviderID, v)) } +// MetadataIsNil applies the IsNil predicate on the "metadata" field. +func MetadataIsNil() predicate.PaymentOrderRecipient { + return predicate.PaymentOrderRecipient(sql.FieldIsNull(FieldMetadata)) +} + +// MetadataNotNil applies the NotNil predicate on the "metadata" field. +func MetadataNotNil() predicate.PaymentOrderRecipient { + return predicate.PaymentOrderRecipient(sql.FieldNotNull(FieldMetadata)) +} + // HasPaymentOrder applies the HasEdge predicate on the "payment_order" edge. func HasPaymentOrder() predicate.PaymentOrderRecipient { return predicate.PaymentOrderRecipient(func(s *sql.Selector) { diff --git a/ent/paymentorderrecipient_create.go b/ent/paymentorderrecipient_create.go index c536b19e..5fd768f6 100644 --- a/ent/paymentorderrecipient_create.go +++ b/ent/paymentorderrecipient_create.go @@ -69,6 +69,12 @@ func (porc *PaymentOrderRecipientCreate) SetNillableProviderID(s *string) *Payme return porc } +// SetMetadata sets the "metadata" field. +func (porc *PaymentOrderRecipientCreate) SetMetadata(m map[string]interface{}) *PaymentOrderRecipientCreate { + porc.mutation.SetMetadata(m) + return porc +} + // SetPaymentOrderID sets the "payment_order" edge to the PaymentOrder entity by ID. func (porc *PaymentOrderRecipientCreate) SetPaymentOrderID(id uuid.UUID) *PaymentOrderRecipientCreate { porc.mutation.SetPaymentOrderID(id) @@ -173,6 +179,10 @@ func (porc *PaymentOrderRecipientCreate) createSpec() (*PaymentOrderRecipient, * _spec.SetField(paymentorderrecipient.FieldProviderID, field.TypeString, value) _node.ProviderID = value } + if value, ok := porc.mutation.Metadata(); ok { + _spec.SetField(paymentorderrecipient.FieldMetadata, field.TypeJSON, value) + _node.Metadata = value + } if nodes := porc.mutation.PaymentOrderIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2O, @@ -314,6 +324,24 @@ func (u *PaymentOrderRecipientUpsert) ClearProviderID() *PaymentOrderRecipientUp return u } +// SetMetadata sets the "metadata" field. +func (u *PaymentOrderRecipientUpsert) SetMetadata(v map[string]interface{}) *PaymentOrderRecipientUpsert { + u.Set(paymentorderrecipient.FieldMetadata, v) + return u +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *PaymentOrderRecipientUpsert) UpdateMetadata() *PaymentOrderRecipientUpsert { + u.SetExcluded(paymentorderrecipient.FieldMetadata) + return u +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *PaymentOrderRecipientUpsert) ClearMetadata() *PaymentOrderRecipientUpsert { + u.SetNull(paymentorderrecipient.FieldMetadata) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -438,6 +466,27 @@ func (u *PaymentOrderRecipientUpsertOne) ClearProviderID() *PaymentOrderRecipien }) } +// SetMetadata sets the "metadata" field. +func (u *PaymentOrderRecipientUpsertOne) SetMetadata(v map[string]interface{}) *PaymentOrderRecipientUpsertOne { + return u.Update(func(s *PaymentOrderRecipientUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *PaymentOrderRecipientUpsertOne) UpdateMetadata() *PaymentOrderRecipientUpsertOne { + return u.Update(func(s *PaymentOrderRecipientUpsert) { + s.UpdateMetadata() + }) +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *PaymentOrderRecipientUpsertOne) ClearMetadata() *PaymentOrderRecipientUpsertOne { + return u.Update(func(s *PaymentOrderRecipientUpsert) { + s.ClearMetadata() + }) +} + // Exec executes the query. func (u *PaymentOrderRecipientUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -725,6 +774,27 @@ func (u *PaymentOrderRecipientUpsertBulk) ClearProviderID() *PaymentOrderRecipie }) } +// SetMetadata sets the "metadata" field. +func (u *PaymentOrderRecipientUpsertBulk) SetMetadata(v map[string]interface{}) *PaymentOrderRecipientUpsertBulk { + return u.Update(func(s *PaymentOrderRecipientUpsert) { + s.SetMetadata(v) + }) +} + +// UpdateMetadata sets the "metadata" field to the value that was provided on create. +func (u *PaymentOrderRecipientUpsertBulk) UpdateMetadata() *PaymentOrderRecipientUpsertBulk { + return u.Update(func(s *PaymentOrderRecipientUpsert) { + s.UpdateMetadata() + }) +} + +// ClearMetadata clears the value of the "metadata" field. +func (u *PaymentOrderRecipientUpsertBulk) ClearMetadata() *PaymentOrderRecipientUpsertBulk { + return u.Update(func(s *PaymentOrderRecipientUpsert) { + s.ClearMetadata() + }) +} + // Exec executes the query. func (u *PaymentOrderRecipientUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/ent/paymentorderrecipient_update.go b/ent/paymentorderrecipient_update.go index 7b69f44f..5603a3cb 100644 --- a/ent/paymentorderrecipient_update.go +++ b/ent/paymentorderrecipient_update.go @@ -111,6 +111,18 @@ func (poru *PaymentOrderRecipientUpdate) ClearProviderID() *PaymentOrderRecipien return poru } +// SetMetadata sets the "metadata" field. +func (poru *PaymentOrderRecipientUpdate) SetMetadata(m map[string]interface{}) *PaymentOrderRecipientUpdate { + poru.mutation.SetMetadata(m) + return poru +} + +// ClearMetadata clears the value of the "metadata" field. +func (poru *PaymentOrderRecipientUpdate) ClearMetadata() *PaymentOrderRecipientUpdate { + poru.mutation.ClearMetadata() + return poru +} + // SetPaymentOrderID sets the "payment_order" edge to the PaymentOrder entity by ID. func (poru *PaymentOrderRecipientUpdate) SetPaymentOrderID(id uuid.UUID) *PaymentOrderRecipientUpdate { poru.mutation.SetPaymentOrderID(id) @@ -201,6 +213,12 @@ func (poru *PaymentOrderRecipientUpdate) sqlSave(ctx context.Context) (n int, er if poru.mutation.ProviderIDCleared() { _spec.ClearField(paymentorderrecipient.FieldProviderID, field.TypeString) } + if value, ok := poru.mutation.Metadata(); ok { + _spec.SetField(paymentorderrecipient.FieldMetadata, field.TypeJSON, value) + } + if poru.mutation.MetadataCleared() { + _spec.ClearField(paymentorderrecipient.FieldMetadata, field.TypeJSON) + } if poru.mutation.PaymentOrderCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2O, @@ -332,6 +350,18 @@ func (poruo *PaymentOrderRecipientUpdateOne) ClearProviderID() *PaymentOrderReci return poruo } +// SetMetadata sets the "metadata" field. +func (poruo *PaymentOrderRecipientUpdateOne) SetMetadata(m map[string]interface{}) *PaymentOrderRecipientUpdateOne { + poruo.mutation.SetMetadata(m) + return poruo +} + +// ClearMetadata clears the value of the "metadata" field. +func (poruo *PaymentOrderRecipientUpdateOne) ClearMetadata() *PaymentOrderRecipientUpdateOne { + poruo.mutation.ClearMetadata() + return poruo +} + // SetPaymentOrderID sets the "payment_order" edge to the PaymentOrder entity by ID. func (poruo *PaymentOrderRecipientUpdateOne) SetPaymentOrderID(id uuid.UUID) *PaymentOrderRecipientUpdateOne { poruo.mutation.SetPaymentOrderID(id) @@ -452,6 +482,12 @@ func (poruo *PaymentOrderRecipientUpdateOne) sqlSave(ctx context.Context) (_node if poruo.mutation.ProviderIDCleared() { _spec.ClearField(paymentorderrecipient.FieldProviderID, field.TypeString) } + if value, ok := poruo.mutation.Metadata(); ok { + _spec.SetField(paymentorderrecipient.FieldMetadata, field.TypeJSON, value) + } + if poruo.mutation.MetadataCleared() { + _spec.ClearField(paymentorderrecipient.FieldMetadata, field.TypeJSON) + } if poruo.mutation.PaymentOrderCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2O, diff --git a/ent/providerordertoken.go b/ent/providerordertoken.go index 47a3aca7..3539ea27 100644 --- a/ent/providerordertoken.go +++ b/ent/providerordertoken.go @@ -36,6 +36,8 @@ type ProviderOrderToken struct { MaxOrderAmount decimal.Decimal `json:"max_order_amount,omitempty"` // MinOrderAmount holds the value of the "min_order_amount" field. MinOrderAmount decimal.Decimal `json:"min_order_amount,omitempty"` + // RateSlippage holds the value of the "rate_slippage" field. + RateSlippage decimal.Decimal `json:"rate_slippage,omitempty"` // Address holds the value of the "address" field. Address string `json:"address,omitempty"` // Network holds the value of the "network" field. @@ -100,7 +102,7 @@ func (*ProviderOrderToken) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case providerordertoken.FieldFixedConversionRate, providerordertoken.FieldFloatingConversionRate, providerordertoken.FieldMaxOrderAmount, providerordertoken.FieldMinOrderAmount: + case providerordertoken.FieldFixedConversionRate, providerordertoken.FieldFloatingConversionRate, providerordertoken.FieldMaxOrderAmount, providerordertoken.FieldMinOrderAmount, providerordertoken.FieldRateSlippage: values[i] = new(decimal.Decimal) case providerordertoken.FieldID: values[i] = new(sql.NullInt64) @@ -177,6 +179,12 @@ func (pot *ProviderOrderToken) assignValues(columns []string, values []any) erro } else if value != nil { pot.MinOrderAmount = *value } + case providerordertoken.FieldRateSlippage: + if value, ok := values[i].(*decimal.Decimal); !ok { + return fmt.Errorf("unexpected type %T for field rate_slippage", values[i]) + } else if value != nil { + pot.RateSlippage = *value + } case providerordertoken.FieldAddress: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field address", values[i]) @@ -282,6 +290,9 @@ func (pot *ProviderOrderToken) String() string { builder.WriteString("min_order_amount=") builder.WriteString(fmt.Sprintf("%v", pot.MinOrderAmount)) builder.WriteString(", ") + builder.WriteString("rate_slippage=") + builder.WriteString(fmt.Sprintf("%v", pot.RateSlippage)) + builder.WriteString(", ") builder.WriteString("address=") builder.WriteString(pot.Address) builder.WriteString(", ") diff --git a/ent/providerordertoken/providerordertoken.go b/ent/providerordertoken/providerordertoken.go index 3b812750..ab1aff61 100644 --- a/ent/providerordertoken/providerordertoken.go +++ b/ent/providerordertoken/providerordertoken.go @@ -29,6 +29,8 @@ const ( FieldMaxOrderAmount = "max_order_amount" // FieldMinOrderAmount holds the string denoting the min_order_amount field in the database. FieldMinOrderAmount = "min_order_amount" + // FieldRateSlippage holds the string denoting the rate_slippage field in the database. + FieldRateSlippage = "rate_slippage" // FieldAddress holds the string denoting the address field in the database. FieldAddress = "address" // FieldNetwork holds the string denoting the network field in the database. @@ -74,6 +76,7 @@ var Columns = []string{ FieldConversionRateType, FieldMaxOrderAmount, FieldMinOrderAmount, + FieldRateSlippage, FieldAddress, FieldNetwork, } @@ -176,6 +179,11 @@ func ByMinOrderAmount(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldMinOrderAmount, opts...).ToFunc() } +// ByRateSlippage orders the results by the rate_slippage field. +func ByRateSlippage(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRateSlippage, opts...).ToFunc() +} + // ByAddress orders the results by the address field. func ByAddress(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldAddress, opts...).ToFunc() diff --git a/ent/providerordertoken/where.go b/ent/providerordertoken/where.go index 8c37914d..a69ff498 100644 --- a/ent/providerordertoken/where.go +++ b/ent/providerordertoken/where.go @@ -86,6 +86,11 @@ func MinOrderAmount(v decimal.Decimal) predicate.ProviderOrderToken { return predicate.ProviderOrderToken(sql.FieldEQ(FieldMinOrderAmount, v)) } +// RateSlippage applies equality check predicate on the "rate_slippage" field. It's identical to RateSlippageEQ. +func RateSlippage(v decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldEQ(FieldRateSlippage, v)) +} + // Address applies equality check predicate on the "address" field. It's identical to AddressEQ. func Address(v string) predicate.ProviderOrderToken { return predicate.ProviderOrderToken(sql.FieldEQ(FieldAddress, v)) @@ -356,6 +361,46 @@ func MinOrderAmountLTE(v decimal.Decimal) predicate.ProviderOrderToken { return predicate.ProviderOrderToken(sql.FieldLTE(FieldMinOrderAmount, v)) } +// RateSlippageEQ applies the EQ predicate on the "rate_slippage" field. +func RateSlippageEQ(v decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldEQ(FieldRateSlippage, v)) +} + +// RateSlippageNEQ applies the NEQ predicate on the "rate_slippage" field. +func RateSlippageNEQ(v decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldNEQ(FieldRateSlippage, v)) +} + +// RateSlippageIn applies the In predicate on the "rate_slippage" field. +func RateSlippageIn(vs ...decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldIn(FieldRateSlippage, vs...)) +} + +// RateSlippageNotIn applies the NotIn predicate on the "rate_slippage" field. +func RateSlippageNotIn(vs ...decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldNotIn(FieldRateSlippage, vs...)) +} + +// RateSlippageGT applies the GT predicate on the "rate_slippage" field. +func RateSlippageGT(v decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldGT(FieldRateSlippage, v)) +} + +// RateSlippageGTE applies the GTE predicate on the "rate_slippage" field. +func RateSlippageGTE(v decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldGTE(FieldRateSlippage, v)) +} + +// RateSlippageLT applies the LT predicate on the "rate_slippage" field. +func RateSlippageLT(v decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldLT(FieldRateSlippage, v)) +} + +// RateSlippageLTE applies the LTE predicate on the "rate_slippage" field. +func RateSlippageLTE(v decimal.Decimal) predicate.ProviderOrderToken { + return predicate.ProviderOrderToken(sql.FieldLTE(FieldRateSlippage, v)) +} + // AddressEQ applies the EQ predicate on the "address" field. func AddressEQ(v string) predicate.ProviderOrderToken { return predicate.ProviderOrderToken(sql.FieldEQ(FieldAddress, v)) diff --git a/ent/providerordertoken_create.go b/ent/providerordertoken_create.go index 8db9ff00..8d50c152 100644 --- a/ent/providerordertoken_create.go +++ b/ent/providerordertoken_create.go @@ -85,6 +85,12 @@ func (potc *ProviderOrderTokenCreate) SetMinOrderAmount(d decimal.Decimal) *Prov return potc } +// SetRateSlippage sets the "rate_slippage" field. +func (potc *ProviderOrderTokenCreate) SetRateSlippage(d decimal.Decimal) *ProviderOrderTokenCreate { + potc.mutation.SetRateSlippage(d) + return potc +} + // SetAddress sets the "address" field. func (potc *ProviderOrderTokenCreate) SetAddress(s string) *ProviderOrderTokenCreate { potc.mutation.SetAddress(s) @@ -219,6 +225,9 @@ func (potc *ProviderOrderTokenCreate) check() error { if _, ok := potc.mutation.MinOrderAmount(); !ok { return &ValidationError{Name: "min_order_amount", err: errors.New(`ent: missing required field "ProviderOrderToken.min_order_amount"`)} } + if _, ok := potc.mutation.RateSlippage(); !ok { + return &ValidationError{Name: "rate_slippage", err: errors.New(`ent: missing required field "ProviderOrderToken.rate_slippage"`)} + } if len(potc.mutation.ProviderIDs()) == 0 { return &ValidationError{Name: "provider", err: errors.New(`ent: missing required edge "ProviderOrderToken.provider"`)} } @@ -283,6 +292,10 @@ func (potc *ProviderOrderTokenCreate) createSpec() (*ProviderOrderToken, *sqlgra _spec.SetField(providerordertoken.FieldMinOrderAmount, field.TypeFloat64, value) _node.MinOrderAmount = value } + if value, ok := potc.mutation.RateSlippage(); ok { + _spec.SetField(providerordertoken.FieldRateSlippage, field.TypeFloat64, value) + _node.RateSlippage = value + } if value, ok := potc.mutation.Address(); ok { _spec.SetField(providerordertoken.FieldAddress, field.TypeString, value) _node.Address = value @@ -490,6 +503,24 @@ func (u *ProviderOrderTokenUpsert) AddMinOrderAmount(v decimal.Decimal) *Provide return u } +// SetRateSlippage sets the "rate_slippage" field. +func (u *ProviderOrderTokenUpsert) SetRateSlippage(v decimal.Decimal) *ProviderOrderTokenUpsert { + u.Set(providerordertoken.FieldRateSlippage, v) + return u +} + +// UpdateRateSlippage sets the "rate_slippage" field to the value that was provided on create. +func (u *ProviderOrderTokenUpsert) UpdateRateSlippage() *ProviderOrderTokenUpsert { + u.SetExcluded(providerordertoken.FieldRateSlippage) + return u +} + +// AddRateSlippage adds v to the "rate_slippage" field. +func (u *ProviderOrderTokenUpsert) AddRateSlippage(v decimal.Decimal) *ProviderOrderTokenUpsert { + u.Add(providerordertoken.FieldRateSlippage, v) + return u +} + // SetAddress sets the "address" field. func (u *ProviderOrderTokenUpsert) SetAddress(v string) *ProviderOrderTokenUpsert { u.Set(providerordertoken.FieldAddress, v) @@ -683,6 +714,27 @@ func (u *ProviderOrderTokenUpsertOne) UpdateMinOrderAmount() *ProviderOrderToken }) } +// SetRateSlippage sets the "rate_slippage" field. +func (u *ProviderOrderTokenUpsertOne) SetRateSlippage(v decimal.Decimal) *ProviderOrderTokenUpsertOne { + return u.Update(func(s *ProviderOrderTokenUpsert) { + s.SetRateSlippage(v) + }) +} + +// AddRateSlippage adds v to the "rate_slippage" field. +func (u *ProviderOrderTokenUpsertOne) AddRateSlippage(v decimal.Decimal) *ProviderOrderTokenUpsertOne { + return u.Update(func(s *ProviderOrderTokenUpsert) { + s.AddRateSlippage(v) + }) +} + +// UpdateRateSlippage sets the "rate_slippage" field to the value that was provided on create. +func (u *ProviderOrderTokenUpsertOne) UpdateRateSlippage() *ProviderOrderTokenUpsertOne { + return u.Update(func(s *ProviderOrderTokenUpsert) { + s.UpdateRateSlippage() + }) +} + // SetAddress sets the "address" field. func (u *ProviderOrderTokenUpsertOne) SetAddress(v string) *ProviderOrderTokenUpsertOne { return u.Update(func(s *ProviderOrderTokenUpsert) { @@ -1048,6 +1100,27 @@ func (u *ProviderOrderTokenUpsertBulk) UpdateMinOrderAmount() *ProviderOrderToke }) } +// SetRateSlippage sets the "rate_slippage" field. +func (u *ProviderOrderTokenUpsertBulk) SetRateSlippage(v decimal.Decimal) *ProviderOrderTokenUpsertBulk { + return u.Update(func(s *ProviderOrderTokenUpsert) { + s.SetRateSlippage(v) + }) +} + +// AddRateSlippage adds v to the "rate_slippage" field. +func (u *ProviderOrderTokenUpsertBulk) AddRateSlippage(v decimal.Decimal) *ProviderOrderTokenUpsertBulk { + return u.Update(func(s *ProviderOrderTokenUpsert) { + s.AddRateSlippage(v) + }) +} + +// UpdateRateSlippage sets the "rate_slippage" field to the value that was provided on create. +func (u *ProviderOrderTokenUpsertBulk) UpdateRateSlippage() *ProviderOrderTokenUpsertBulk { + return u.Update(func(s *ProviderOrderTokenUpsert) { + s.UpdateRateSlippage() + }) +} + // SetAddress sets the "address" field. func (u *ProviderOrderTokenUpsertBulk) SetAddress(v string) *ProviderOrderTokenUpsertBulk { return u.Update(func(s *ProviderOrderTokenUpsert) { diff --git a/ent/providerordertoken_update.go b/ent/providerordertoken_update.go index b409eb29..8e3e45fb 100644 --- a/ent/providerordertoken_update.go +++ b/ent/providerordertoken_update.go @@ -137,6 +137,27 @@ func (potu *ProviderOrderTokenUpdate) AddMinOrderAmount(d decimal.Decimal) *Prov return potu } +// SetRateSlippage sets the "rate_slippage" field. +func (potu *ProviderOrderTokenUpdate) SetRateSlippage(d decimal.Decimal) *ProviderOrderTokenUpdate { + potu.mutation.ResetRateSlippage() + potu.mutation.SetRateSlippage(d) + return potu +} + +// SetNillableRateSlippage sets the "rate_slippage" field if the given value is not nil. +func (potu *ProviderOrderTokenUpdate) SetNillableRateSlippage(d *decimal.Decimal) *ProviderOrderTokenUpdate { + if d != nil { + potu.SetRateSlippage(*d) + } + return potu +} + +// AddRateSlippage adds d to the "rate_slippage" field. +func (potu *ProviderOrderTokenUpdate) AddRateSlippage(d decimal.Decimal) *ProviderOrderTokenUpdate { + potu.mutation.AddRateSlippage(d) + return potu +} + // SetAddress sets the "address" field. func (potu *ProviderOrderTokenUpdate) SetAddress(s string) *ProviderOrderTokenUpdate { potu.mutation.SetAddress(s) @@ -330,6 +351,12 @@ func (potu *ProviderOrderTokenUpdate) sqlSave(ctx context.Context) (n int, err e if value, ok := potu.mutation.AddedMinOrderAmount(); ok { _spec.AddField(providerordertoken.FieldMinOrderAmount, field.TypeFloat64, value) } + if value, ok := potu.mutation.RateSlippage(); ok { + _spec.SetField(providerordertoken.FieldRateSlippage, field.TypeFloat64, value) + } + if value, ok := potu.mutation.AddedRateSlippage(); ok { + _spec.AddField(providerordertoken.FieldRateSlippage, field.TypeFloat64, value) + } if value, ok := potu.mutation.Address(); ok { _spec.SetField(providerordertoken.FieldAddress, field.TypeString, value) } @@ -553,6 +580,27 @@ func (potuo *ProviderOrderTokenUpdateOne) AddMinOrderAmount(d decimal.Decimal) * return potuo } +// SetRateSlippage sets the "rate_slippage" field. +func (potuo *ProviderOrderTokenUpdateOne) SetRateSlippage(d decimal.Decimal) *ProviderOrderTokenUpdateOne { + potuo.mutation.ResetRateSlippage() + potuo.mutation.SetRateSlippage(d) + return potuo +} + +// SetNillableRateSlippage sets the "rate_slippage" field if the given value is not nil. +func (potuo *ProviderOrderTokenUpdateOne) SetNillableRateSlippage(d *decimal.Decimal) *ProviderOrderTokenUpdateOne { + if d != nil { + potuo.SetRateSlippage(*d) + } + return potuo +} + +// AddRateSlippage adds d to the "rate_slippage" field. +func (potuo *ProviderOrderTokenUpdateOne) AddRateSlippage(d decimal.Decimal) *ProviderOrderTokenUpdateOne { + potuo.mutation.AddRateSlippage(d) + return potuo +} + // SetAddress sets the "address" field. func (potuo *ProviderOrderTokenUpdateOne) SetAddress(s string) *ProviderOrderTokenUpdateOne { potuo.mutation.SetAddress(s) @@ -776,6 +824,12 @@ func (potuo *ProviderOrderTokenUpdateOne) sqlSave(ctx context.Context) (_node *P if value, ok := potuo.mutation.AddedMinOrderAmount(); ok { _spec.AddField(providerordertoken.FieldMinOrderAmount, field.TypeFloat64, value) } + if value, ok := potuo.mutation.RateSlippage(); ok { + _spec.SetField(providerordertoken.FieldRateSlippage, field.TypeFloat64, value) + } + if value, ok := potuo.mutation.AddedRateSlippage(); ok { + _spec.AddField(providerordertoken.FieldRateSlippage, field.TypeFloat64, value) + } if value, ok := potuo.mutation.Address(); ok { _spec.SetField(providerordertoken.FieldAddress, field.TypeString, value) } diff --git a/ent/runtime/runtime.go b/ent/runtime/runtime.go index bdc56118..df13cbb5 100644 --- a/ent/runtime/runtime.go +++ b/ent/runtime/runtime.go @@ -122,7 +122,7 @@ func init() { // linkedaddress.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. linkedaddress.UpdateDefaultUpdatedAt = linkedaddressDescUpdatedAt.UpdateDefault.(func() time.Time) // linkedaddressDescTxHash is the schema descriptor for tx_hash field. - linkedaddressDescTxHash := linkedaddressFields[7].Descriptor() + linkedaddressDescTxHash := linkedaddressFields[8].Descriptor() // linkedaddress.TxHashValidator is a validator for the "tx_hash" field. It is called by the builders before save. linkedaddress.TxHashValidator = linkedaddressDescTxHash.Validators[0].(func(string) error) lockorderfulfillmentMixin := schema.LockOrderFulfillment{}.Mixin() @@ -164,11 +164,11 @@ func init() { // lockpaymentorder.TxHashValidator is a validator for the "tx_hash" field. It is called by the builders before save. lockpaymentorder.TxHashValidator = lockpaymentorderDescTxHash.Validators[0].(func(string) error) // lockpaymentorderDescCancellationCount is the schema descriptor for cancellation_count field. - lockpaymentorderDescCancellationCount := lockpaymentorderFields[12].Descriptor() + lockpaymentorderDescCancellationCount := lockpaymentorderFields[13].Descriptor() // lockpaymentorder.DefaultCancellationCount holds the default value on creation for the cancellation_count field. lockpaymentorder.DefaultCancellationCount = lockpaymentorderDescCancellationCount.Default.(int) // lockpaymentorderDescCancellationReasons is the schema descriptor for cancellation_reasons field. - lockpaymentorderDescCancellationReasons := lockpaymentorderFields[13].Descriptor() + lockpaymentorderDescCancellationReasons := lockpaymentorderFields[14].Descriptor() // lockpaymentorder.DefaultCancellationReasons holds the default value on creation for the cancellation_reasons field. lockpaymentorder.DefaultCancellationReasons = lockpaymentorderDescCancellationReasons.Default.([]string) // lockpaymentorderDescID is the schema descriptor for id field. diff --git a/ent/schema/linkedaddress.go b/ent/schema/linkedaddress.go index 1d25f6e1..9d84f12c 100644 --- a/ent/schema/linkedaddress.go +++ b/ent/schema/linkedaddress.go @@ -30,6 +30,8 @@ func (LinkedAddress) Fields() []ent.Field { field.String("institution"), field.String("account_identifier"), field.String("account_name"), + field.JSON("metadata", map[string]interface{}{}). + Optional(), field.String("owner_address"). Unique(), field.Int64("last_indexed_block"). diff --git a/ent/schema/lockpaymentorder.go b/ent/schema/lockpaymentorder.go index 4f9bfea9..930e1780 100644 --- a/ent/schema/lockpaymentorder.go +++ b/ent/schema/lockpaymentorder.go @@ -46,6 +46,8 @@ func (LockPaymentOrder) Fields() []ent.Field { field.String("account_name"), field.String("memo"). Optional(), + field.JSON("metadata", map[string]interface{}{}). + Optional(), field.Int("cancellation_count"). Default(0), field.Strings("cancellation_reasons"). diff --git a/ent/schema/paymentorderrecipient.go b/ent/schema/paymentorderrecipient.go index 1e712ab3..f03c3e39 100644 --- a/ent/schema/paymentorderrecipient.go +++ b/ent/schema/paymentorderrecipient.go @@ -21,6 +21,8 @@ func (PaymentOrderRecipient) Fields() []ent.Field { Optional(), field.String("provider_id"). Optional(), + field.JSON("metadata", map[string]interface{}{}). + Optional(), } } diff --git a/ent/schema/providerordertoken.go b/ent/schema/providerordertoken.go index f9e7a9f9..f2a3b473 100644 --- a/ent/schema/providerordertoken.go +++ b/ent/schema/providerordertoken.go @@ -33,6 +33,8 @@ func (ProviderOrderToken) Fields() []ent.Field { GoType(decimal.Decimal{}), field.Float("min_order_amount"). GoType(decimal.Decimal{}), + field.Float("rate_slippage"). + GoType(decimal.Decimal{}), field.String("address").Optional(), field.String("network").Optional(), } diff --git a/go.mod b/go.mod index 730abaca..f4dc8fb8 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/spf13/viper v1.16.0 github.com/stackup-wallet/stackup-bundler v0.6.30 github.com/stretchr/testify v1.8.4 + github.com/xeipuuv/gojsonschema v1.2.0 google.golang.org/grpc v1.55.0 ) @@ -87,6 +88,8 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/tyler-smith/go-bip32 v1.0.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect go.uber.org/atomic v1.9.0 // indirect diff --git a/go.sum b/go.sum index acdd581a..ee6faa31 100644 --- a/go.sum +++ b/go.sum @@ -566,8 +566,6 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -628,8 +626,11 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= diff --git a/main.go b/main.go index 99b8de5e..b766e10c 100644 --- a/main.go +++ b/main.go @@ -20,11 +20,9 @@ func main() { // Connect to the database DSN := config.DBConfig() - if err := storage.DBConnection(DSN); err != nil { logger.Fatalf("database DBConnection: %s", err) } - defer storage.GetClient().Close() // err := tasks.FixDatabaseMisHap() diff --git a/routers/middleware/auth.go b/routers/middleware/auth.go index 1e3e73a7..2c967550 100644 --- a/routers/middleware/auth.go +++ b/routers/middleware/auth.go @@ -89,6 +89,7 @@ func JWTMiddleware(c *gin.Context) { Where(providerprofile.HasUserWith(user.IDEQ(userUUID))). Only(c) if err != nil && !senderAndProvider { + fmt.Println("error", err) c.Set("provider", nil) } diff --git a/routers/router.go b/routers/router.go index 2f1cded6..5bc8bc74 100644 --- a/routers/router.go +++ b/routers/router.go @@ -27,7 +27,7 @@ func Routes() *gin.Engine { router.Use(gin.Logger()) router.Use(gin.Recovery()) router.Use(middleware.CORSMiddleware()) - router.Use(middleware.RateLimitMiddleware()) + // router.Use(middleware.RateLimitMiddleware()) RegisterRoutes(router) //routes register diff --git a/scripts/db_data/dump.sql b/scripts/db_data/dump.sql index 7184ab9a..c71ba8e8 100644 --- a/scripts/db_data/dump.sql +++ b/scripts/db_data/dump.sql @@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS "public"."atlas_schema_revisions" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."ent_types" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "type" varchar NOT NULL, PRIMARY KEY ("id") ); @@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS "public"."identity_verification_requests" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."institutions" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "code" varchar NOT NULL, @@ -101,7 +101,7 @@ CREATE TABLE IF NOT EXISTS "public"."institutions" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."linked_addresses" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "address" varchar NOT NULL, @@ -155,7 +155,7 @@ CREATE TABLE IF NOT EXISTS "public"."lock_payment_orders" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."networks" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "chain_id" int8 NOT NULL, @@ -172,7 +172,7 @@ CREATE TABLE IF NOT EXISTS "public"."networks" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."payment_order_recipients" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "institution" varchar NOT NULL, "account_identifier" varchar NOT NULL, "account_name" varchar NOT NULL, @@ -214,7 +214,7 @@ CREATE TABLE IF NOT EXISTS "public"."payment_orders" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."provider_order_tokens" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "fixed_conversion_rate" float8 NOT NULL, @@ -255,7 +255,7 @@ CREATE TABLE IF NOT EXISTS "public"."provider_profiles" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."provider_ratings" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "trust_score" float8 NOT NULL, @@ -272,7 +272,7 @@ CREATE TABLE IF NOT EXISTS "public"."provision_bucket_provider_profiles" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."provision_buckets" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "min_amount" float8 NOT NULL, "max_amount" float8 NOT NULL, "created_at" timestamptz NOT NULL, @@ -282,7 +282,7 @@ CREATE TABLE IF NOT EXISTS "public"."provision_buckets" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."receive_addresses" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "address" varchar NOT NULL, @@ -298,14 +298,14 @@ CREATE TABLE IF NOT EXISTS "public"."receive_addresses" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."sender_order_tokens" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "fee_percent" float8 NOT NULL, "fee_address" varchar NOT NULL, "refund_address" varchar NOT NULL, "sender_profile_order_tokens" uuid NOT NULL, - "token_sender_settings" int8 NOT NULL, + "token_sender_order_tokens" int8 NOT NULL, PRIMARY KEY ("id") ); @@ -324,7 +324,7 @@ CREATE TABLE IF NOT EXISTS "public"."sender_profiles" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."tokens" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "symbol" varchar NOT NULL, @@ -379,7 +379,7 @@ CREATE TABLE IF NOT EXISTS "public"."verification_tokens" ( -- Table Definition CREATE TABLE IF NOT EXISTS "public"."webhook_retry_attempts" ( - "id" int8 NOT NULL, + "id" int8 GENERATED BY DEFAULT AS IDENTITY NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "attempt_number" int8 NOT NULL, @@ -586,7 +586,7 @@ INSERT INTO "public"."networks" ("id", "created_at", "updated_at", "chain_id", " (17179869184, '2024-02-03 23:54:30.744922+00', '2024-02-03 23:54:30.744923+00', 11155111, 'ethereum-sepolia', 'wss://sepolia.infura.io/ws/v3/4458cf4d1689497b9a38b1d6bbf05e78', 't', 0, NULL, '0xCAD53Ff499155Cc2fAA2082A85716322906886c2', 'https://bundler.biconomy.io/api/v2/11155111/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44', 'https://paymaster.biconomy.io/api/v1/11155111/VD_kir2JP.9bd5c901-92c0-43ca-a30f-bb8bbbdd3286'), (17179869185, '2024-04-29 10:39:32.596797+00', '2024-04-29 10:39:32.596797+00', 421614, 'arbitrum-sepolia', 'wss://arbitrum-sepolia.infura.io/ws/v3/4458cf4d1689497b9a38b1d6bbf05e78', 't', 5, '0x66eee', '0x87B321fc77A0fDD0ca1fEe7Ab791131157B9841A', 'https://bundler.biconomy.io/api/v2/421614/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44', 'https://paymaster.biconomy.io/api/v1/421614/oUg0lNcC-.5c16e9a1-2991-41c2-86ad-8bbd246ace57'), (17179869186, '2024-06-07 01:45:47.477654+00', '2024-06-07 01:45:47.477654+00', 12002, 'tron-shasta', 'https://api.shasta.trongrid.io', 't', 5, NULL, 'TYA8urq7nkN2yU7rJqAgwDShCusDZrrsxZ', 'https://bundler.shasta.tron.io', 'https://paymaster.shasta.tron.io'), -(17179869187, '2024-06-18 03:04:10.375048+00', '2024-06-18 03:04:10.375048+00', 84532, 'base-sepolia', 'wss://base-sepolia.infura.io/ws/v3/4458cf4d1689497b9a38b1d6bbf05e78', 't', 0, NULL, '0x847dfdAa218F9137229CF8424378871A1DA8f625', 'https://bundler.biconomy.io/api/v2/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44', 'https://paymaster.biconomy.io/api/v1/84532/AtzxDAqR_.805ec674-af7e-4af8-828d-1e4190b51d5a'); +(17179869187, '2024-06-18 03:04:10.375048+00', '2024-06-18 03:04:10.375048+00', 84532, 'base-sepolia', 'wss://base-sepolia.infura.io/ws/v3/4458cf4d1689497b9a38b1d6bbf05e78', 't', 0.03, NULL, '0x847dfdAa218F9137229CF8424378871A1DA8f625', 'https://bundler.biconomy.io/api/v2/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44', 'https://paymaster.biconomy.io/api/v1/84532/AtzxDAqR_.805ec674-af7e-4af8-828d-1e4190b51d5a'); INSERT INTO "public"."tokens" ("id", "created_at", "updated_at", "symbol", "contract_address", "decimals", "is_enabled", "network_tokens", "base_currency") VALUES (55834574849, '2024-03-07 16:38:00.058+00', '2024-03-07 16:38:00.058+00', '6TEST', '0x3870419Ba2BBf0127060bCB37f69A1b1C090992B', 6, 't', 17179869184, 'USD'), @@ -605,12 +605,11 @@ INSERT INTO "public"."api_keys" ("id", "secret", "provider_profile_api_key", "se ('11f93de0-d304-4498-8b7b-6cecbc5b2dd8', '/5OxcB9HZ2jJIfsC+AmHhEVs6Khe3x0KS9ZkhSvHZknOiVtBvEa4J+f8P8nzs9qfComAvogtkcqrzGc+suu6JA3lqbSlazyO', NULL, 'e93a1cba-832f-4a7c-aab5-929a53c84324'); INSERT INTO "public"."provider_order_tokens" ("id", "created_at", "updated_at", "fixed_conversion_rate", "floating_conversion_rate", "conversion_rate_type", "max_order_amount", "min_order_amount", "provider_profile_order_tokens", "address", "network", "fiat_currency_provider_order_tokens", "token_provider_order_tokens") VALUES -(30064771084, '2025-01-21 12:45:59.108096+00', '2025-01-21 16:39:25.205819+00', 1598, 0, 'fixed', 900, 100, 'AtGaDPqT', '0xf4c5c4deDde7A86b25E7430796441e209e23eBFB', 'ethereum-sepolia', '5a349408-ebcf-4c7e-98c7-46b6596e0b27', 55834574849), -(30064771085, '2025-01-21 12:46:12.789689+00', '2025-01-21 12:46:12.78969+00', 1598, 0, 'fixed', 900, 100, 'AtGaDPqT', '0xf4c5c4deDde7A86b25E7430796441e209e23eBFB', 'base-sepolia', '5a349408-ebcf-4c7e-98c7-46b6596e0b27', 55834574852), -(30064771086, '2025-01-21 12:46:20.289845+00', '2025-01-21 12:46:20.289845+00', 1598, 0, 'fixed', 900, 100, 'AtGaDPqT', '0xf4c5c4deDde7A86b25E7430796441e209e23eBFB', 'arbitrum-sepolia', '5a349408-ebcf-4c7e-98c7-46b6596e0b27', 55834574853); +(32, '2025-01-21 12:46:12.789689+00', '2025-01-21 12:46:12.78969+00', 0, 0, 'floating', 900, 0.5, 'AtGaDPqT', '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', 'base-sepolia', '5a349408-ebcf-4c7e-98c7-46b6596e0b27', 55834574852), +(14, '2025-01-21 12:46:12.789689+00', '2025-01-21 12:46:12.78969+00', 0, 0, 'floating', 900, 0.5, 'AtGaDPqT', '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', 'base-sepolia', '5a349408-ebcf-4c7e-98c7-46b6596e0b27', 55834574851); INSERT INTO "public"."provider_profiles" ("id", "trading_name", "host_identifier", "provision_mode", "is_active", "is_available", "updated_at", "visibility_mode", "address", "mobile_number", "date_of_birth", "business_name", "identity_document_type", "identity_document", "business_document", "user_provider_profile", "is_kyb_verified") VALUES -('AtGaDPqT', 'John Doe Exchange', 'http://localhost:8105', 'auto', 't', 't', '2025-01-21 16:39:25.232449+00', 'private', '1, John Doe Street, Surulere, Lagos, Nigeria', '+2348123456789', '1993-01-01 00:00:00+00', 'John Doe Exchange Ltd', 'passport', 'https://res.cloudinary.com/de6e0wihu/image/upload/v1737463231/wbeica7nxawqthdnpazv.png', 'https://res.cloudinary.com/de6e0wihu/image/upload/v1737463231/hngwr0f5mw1z9vdvrjqm.png', '6f7209d3-8f70-499f-aec8-65644d55ad5e', 't'); +('AtGaDPqT', 'John Doe Exchange', 'http://host.docker.internal:8105', 'auto', 't', 't', '2025-01-21 16:39:25.232449+00', 'private', '1, John Doe Street, Surulere, Lagos, Nigeria', '+2348123456789', '1993-01-01 00:00:00+00', 'John Doe Exchange Ltd', 'passport', 'https://res.cloudinary.com/de6e0wihu/image/upload/v1737463231/wbeica7nxawqthdnpazv.png', 'https://res.cloudinary.com/de6e0wihu/image/upload/v1737463231/hngwr0f5mw1z9vdvrjqm.png', '6f7209d3-8f70-499f-aec8-65644d55ad5e', 't'); INSERT INTO "public"."provision_buckets" ("id", "min_amount", "max_amount", "created_at", "fiat_currency_provision_buckets") VALUES (42949672960, 5001, 50000, '2024-02-03 23:54:34.688488+00', '5a349408-ebcf-4c7e-98c7-46b6596e0b27'), @@ -658,7 +657,7 @@ INSERT INTO "public"."provision_bucket_provider_profiles" ("provision_bucket_id" (42949672979, 'AtGaDPqT'), (42949672980, 'AtGaDPqT'); -INSERT INTO "public"."sender_order_tokens" ("id", "created_at", "updated_at", "fee_percent", "fee_address", "refund_address", "sender_profile_order_tokens", "token_sender_settings") VALUES +INSERT INTO "public"."sender_order_tokens" ("id", "created_at", "updated_at", "fee_percent", "fee_address", "refund_address", "sender_profile_order_tokens", "token_sender_order_tokens") VALUES (81604378631, '2025-01-21 16:41:02.671992+00', '2025-01-21 16:41:02.671993+00', 0, '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', 'e93a1cba-832f-4a7c-aab5-929a53c84324', 55834574849), (81604378632, '2025-01-21 16:41:02.689905+00', '2025-01-21 16:41:02.689905+00', 0, '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', 'e93a1cba-832f-4a7c-aab5-929a53c84324', 55834574850), (81604378633, '2025-01-21 16:41:20.630752+00', '2025-01-21 16:41:20.630752+00', 0, '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', '0x409689E3008d43a9eb439e7B275749D4a71D8E2D', 'e93a1cba-832f-4a7c-aab5-929a53c84324', 55834574853), @@ -756,12 +755,12 @@ ALTER TABLE "public"."receive_addresses" ADD FOREIGN KEY ("payment_order_receive CREATE UNIQUE INDEX receive_addresses_address_key ON public.receive_addresses USING btree (address); CREATE UNIQUE INDEX receive_addresses_salt_key ON public.receive_addresses USING btree (salt); CREATE UNIQUE INDEX receive_addresses_payment_order_receive_address_key ON public.receive_addresses USING btree (payment_order_receive_address); -ALTER TABLE "public"."sender_order_tokens" ADD FOREIGN KEY ("token_sender_settings") REFERENCES "public"."tokens"("id"); +ALTER TABLE "public"."sender_order_tokens" ADD FOREIGN KEY ("token_sender_order_tokens") REFERENCES "public"."tokens"("id"); ALTER TABLE "public"."sender_order_tokens" ADD FOREIGN KEY ("sender_profile_order_tokens") REFERENCES "public"."sender_profiles"("id") ON DELETE CASCADE; -- Indices -CREATE UNIQUE INDEX senderordertoken_sender_profil_c0e12093989225f7a56a29b8ff69c3bf ON public.sender_order_tokens USING btree (sender_profile_order_tokens, token_sender_settings); +CREATE UNIQUE INDEX senderordertoken_sender_profil_c0e12093989225f7a56a29b8ff69c3bf ON public.sender_order_tokens USING btree (sender_profile_order_tokens, token_sender_order_tokens); ALTER TABLE "public"."sender_profiles" ADD FOREIGN KEY ("user_sender_profile") REFERENCES "public"."users"("id") ON DELETE CASCADE; diff --git a/scripts/import_db.sh b/scripts/import_db.sh index e8f4fe83..2a867620 100755 --- a/scripts/import_db.sh +++ b/scripts/import_db.sh @@ -79,7 +79,7 @@ export PGPASSWORD="$DB_PASSWORD" import_sql() { local file="$1" echo "Importing $file..." - PGSSLMODE=require psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" \ + PGSSLMODE=prefer psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" \ --set ON_ERROR_STOP=1 \ -f "$file" @@ -97,7 +97,7 @@ echo "Using database: $DB_NAME on $DB_HOST:$DB_PORT" # Test connection first echo "Testing database connection..." -if ! PGSSLMODE=require psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" -c '\q'; then +if ! PGSSLMODE=prefer psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" -c '\q'; then echo "Error: Could not connect to database. Please check your connection parameters." exit 1 fi diff --git a/services/email.go b/services/email.go index db4c6439..5cca10d3 100644 --- a/services/email.go +++ b/services/email.go @@ -164,13 +164,13 @@ func SendTemplateEmail(content types.SendEmailPayload, templateId string) (types Body().AsJSON(reqBody). Send() if err != nil { - logger.Errorf("error sending request: %v", err) + logger.Errorf("Failed to send Email: %v", err) return types.SendEmailResponse{}, fmt.Errorf("error sending request: %w", err) } data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("error parsing response: %v %v", err, data) + logger.Errorf("Failed to decode %v response after sending Email: %v", data, err) return types.SendEmailResponse{}, fmt.Errorf("error parsing response: %w", err) } @@ -215,13 +215,13 @@ func SendTemplateEmailWithJsonAttachment(content types.SendEmailPayload, templat Body().AsJSON(reqBody). Send() if err != nil { - logger.Errorf("error sending request: %v", err) + logger.Errorf("Failed to send Email with JSON attachment: %v", err) return fmt.Errorf("error sending request: %w", err) } data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("error parsing response: %v %v", err, data) + logger.Errorf("Failed to decode %v response after sending Email with JSON attachment: %v", data, err) return fmt.Errorf("error parsing response: %w", err) } diff --git a/services/indexer.go b/services/indexer.go index b5591501..f2addf5f 100644 --- a/services/indexer.go +++ b/services/indexer.go @@ -27,7 +27,6 @@ import ( "github.com/paycrest/aggregator/ent/provisionbucket" "github.com/paycrest/aggregator/ent/receiveaddress" "github.com/paycrest/aggregator/ent/senderprofile" - "github.com/paycrest/aggregator/ent/token" tokenEnt "github.com/paycrest/aggregator/ent/token" "github.com/paycrest/aggregator/ent/transactionlog" "github.com/paycrest/aggregator/ent/user" @@ -97,7 +96,12 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP // Initialize contract filterer filterer, err := contracts.NewERC20TokenFilterer(common.HexToAddress(token.ContractAddress), client) if err != nil { - logger.Errorf("IndexERC20Transfer.NewERC20TokenFilterer(%s): %v", token.Edges.Network.Identifier, err) + // Need to group by network + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": token.ContractAddress, + "ReceiveAddress": addressToWatch, + }).Errorf("Failed to index ERC20 transfers for %s", token.Edges.Network.Identifier) return err } @@ -136,7 +140,13 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP End: &toBlock, }, nil, addresses) if err != nil { - logger.Errorf("IndexERC20Transfer.FilterTransfer(%s): %v", token.Edges.Network.Identifier, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": token.ContractAddress, + "ReceiveAddress": addressToWatch, + "StartBlock": startBlock, + "EndBlock": toBlock, + }).Errorf("Failed to index ERC20 transfers for %s", token.Edges.Network.Identifier) return err } @@ -162,7 +172,10 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP Only(ctx) if err != nil { if !ent.IsNotFound(err) { - logger.Errorf("IndexERC20Transfer.db: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Address": transferEvent.To, + }).Errorf("Failed to query linked address when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) } } @@ -197,9 +210,13 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP } // Create payment order - institution, err := utils.GetInstitutionByCode(ctx, linkedAddress.Institution) + institution, err := utils.GetInstitutionByCode(ctx, linkedAddress.Institution, true) if err != nil { - logger.Errorf("IndexERC20Transfer.GetInstitutionByCode: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linkedAddress.Address, + "LinkedAddressInstitution": linkedAddress.Institution, + }).Errorf("Failed to get institution when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) continue } @@ -211,7 +228,12 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP if !strings.EqualFold(token.BaseCurrency, institution.Edges.FiatCurrency.Code) { rateResponse, err = utils.GetTokenRateFromQueue(token.Symbol, orderAmount, institution.Edges.FiatCurrency.Code, institution.Edges.FiatCurrency.MarketRate) if err != nil { - logger.Errorf("IndexERC20Transfer.GetTokenRateFromQueue: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Token": token.Symbol, + "LinkedAddressInstitution": linkedAddress.Institution, + "Code": institution.Edges.FiatCurrency.Code, + }).Errorf("Failed to get token rate when indexing ERC20 transfers for %s from queue", token.Edges.Network.Identifier) continue } } else { @@ -220,7 +242,10 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP tx, err := db.Client.Tx(ctx) if err != nil { - logger.Errorf("IndexERC20Transfer.Tx: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linkedAddress.Address, + }).Errorf("Failed to create transaction when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) continue } @@ -261,7 +286,10 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP // AddTransactions(transactionLog). Save(ctx) if err != nil { - logger.Errorf("IndexERC20Transfer.CreatePaymentOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linkedAddress.Address, + }).Errorf("Failed to create payment order when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) _ = tx.Rollback() continue } @@ -271,10 +299,14 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP SetInstitution(linkedAddress.Institution). SetAccountIdentifier(linkedAddress.AccountIdentifier). SetAccountName(linkedAddress.AccountName). + SetMetadata(linkedAddress.Metadata). SetPaymentOrder(order). Save(ctx) if err != nil { - logger.Errorf("IndexERC20Transfer.CreatePaymentOrderRecipient: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linkedAddress.Address, + }).Errorf("Failed to create payment order recipient when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) _ = tx.Rollback() continue } @@ -285,19 +317,28 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP SetLastIndexedBlock(int64(transferEvent.BlockNumber)). Save(ctx) if err != nil { - logger.Errorf("IndexERC20Transfer.UpdateLinkedAddress: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linkedAddress.Address, + }).Errorf("Failed to update linked address when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) _ = tx.Rollback() continue } if err := tx.Commit(); err != nil { - logger.Errorf("IndexERC20Transfer.Commit: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "LinkedAddress": linkedAddress.Address, + }).Errorf("Failed to commit transaction when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) continue } err = s.order.CreateOrder(ctx, client, order.ID) if err != nil { - logger.Errorf("IndexERC20Transfer.CreateOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + }).Errorf("Failed to create order when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) continue } @@ -305,8 +346,11 @@ func (s *IndexerService) IndexERC20Transfer(ctx context.Context, client types.RP // Process transfer event for receive address done, err := s.UpdateReceiveAddressStatus(ctx, client, order.Edges.ReceiveAddress, order, transferEvent) if err != nil { - if !strings.Contains(err.Error(), "Duplicate payment order") { - logger.Errorf("IndexERC20Transfer.UpdateReceiveAddressStatus: %v", err) + if !strings.Contains(fmt.Sprintf("%v", err), "Duplicate payment order") { + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + }).Errorf("Failed to update receive address status when indexing ERC20 transfers for %s", token.Edges.Network.Identifier) } continue } @@ -337,13 +381,19 @@ func (s *IndexerService) IndexTRC20Transfer(ctx context.Context, order *ent.Paym Retry().Set(3, 1*time.Second). Send() if err != nil { - logger.Errorf("IndexTRC20Transfer.FetchTransfer: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + }).Errorf("Failed to fetch TRC20 transfer for %s", order.Edges.Token.Edges.Network.Identifier) return err } data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("IndexTRC20Transfer.ParseJSONResponse: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Response": data, + }).Errorf("Failed to parse JSON response for TRC20 transfer for %s", order.Edges.Token.Edges.Network.Identifier) return err } @@ -353,7 +403,10 @@ func (s *IndexerService) IndexTRC20Transfer(ctx context.Context, order *ent.Paym value, err := decimal.NewFromString(eventData["value"].(string)) if err != nil { - logger.Errorf("IndexTRC20Transfer.NewFromString: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Value": eventData["value"], + }).Errorf("Failed to parse decimal value from TRC20 transfer for %s", order.Edges.Token.Edges.Network.Identifier) return err } @@ -369,13 +422,19 @@ func (s *IndexerService) IndexTRC20Transfer(ctx context.Context, order *ent.Paym Retry().Set(3, 1*time.Second). Send() if err != nil { - logger.Errorf("IndexTRC20Transfer.FetchBlockNumber: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "TransactionID": eventData["transaction_id"], + }).Errorf("Failed to fetch block number for TRC20 transfer for %s", order.Edges.Token.Edges.Network.Identifier) return err } data, err = utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("IndexTRC20Transfer.ParseJSONResponse: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Response": data, + }).Errorf("Failed to parse JSON response for TRC20 transfer for %s", order.Edges.Token.Edges.Network.Identifier) return err } @@ -391,7 +450,10 @@ func (s *IndexerService) IndexTRC20Transfer(ctx context.Context, order *ent.Paym go func() { _, err := s.UpdateReceiveAddressStatus(ctx, nil, order.Edges.ReceiveAddress, order, transferEvent) if err != nil { - logger.Errorf("IndexTRC20Transfer.UpdateReceiveAddressStatus: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + }).Errorf("Failed to update receive address status when indexing TRC20 transfers for %s", order.Edges.Token.Edges.Network.Identifier) } }() } @@ -416,7 +478,9 @@ func (s *IndexerService) IndexOrderCreated(ctx context.Context, client types.RPC // Initialize contract filterer filterer, err := contracts.NewGatewayFilterer(common.HexToAddress(network.GatewayContractAddress), client) if err != nil { - logger.Errorf("IndexOrderCreated.NewGatewayFilterer: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to create gateway filterer when indexing order created events for %s", network.Identifier) return err } @@ -424,7 +488,9 @@ func (s *IndexerService) IndexOrderCreated(ctx context.Context, client types.RPC header, err := client.HeaderByNumber(ctx, nil) if err != nil { if err != context.Canceled { - logger.Errorf("IndexOrderCreated.HeaderByNumber: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to fetch current block header for %s when indexing order created events", network.Identifier) } return err } @@ -441,7 +507,11 @@ func (s *IndexerService) IndexOrderCreated(ctx context.Context, client types.RPC End: &toBlock, }, nil, nil, nil) if err != nil { - logger.Errorf("IndexOrderCreated.FilterOrderCreated (%s): %v", network.Identifier, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Start": uint64(int64(toBlock) - fromBlock), + "End": toBlock, + }).Errorf("Failed to filter order created events for %s when indexing order created events", network.Identifier) return err } @@ -460,8 +530,11 @@ func (s *IndexerService) IndexOrderCreated(ctx context.Context, client types.RPC err := s.CreateLockPaymentOrder(ctx, client, network, event) if err != nil { - if !strings.Contains(err.Error(), "duplicate key value violates unique constraint") { - logger.Errorf("IndexOrderCreated.CreateLockPaymentOrder: %v", err) + if !strings.Contains(fmt.Sprintf("%v", err), "duplicate key value violates unique constraint") { + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": fmt.Sprintf("0x%v", hex.EncodeToString(event.OrderId[:])), + }).Errorf("Failed to create lock payment order when indexing order created events for %s", network.Identifier) } continue } @@ -493,13 +566,18 @@ func (s *IndexerService) IndexOrderCreatedTron(ctx context.Context, order *ent.P Retry().Set(3, 1*time.Second). Send() if err != nil { - logger.Errorf("fetch txn event logs: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "TxHash": order.TxHash, + }).Errorf("Failed to fetch trx info by id for %s", order.Edges.Token.Edges.Network.Identifier) return err } data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("failed to parse JSON response: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to parse JSON response for %s", order.Edges.Token.Edges.Network.Identifier) return err } @@ -509,7 +587,9 @@ func (s *IndexerService) IndexOrderCreatedTron(ctx context.Context, order *ent.P if eventData["topics"].([]interface{})[0] == "40ccd1ceb111a3c186ef9911e1b876dc1f789ed331b86097b3b8851055b6a137" { unpackedEventData, err := utils.UnpackEventData(eventData["data"].(string), contracts.GatewayMetaData.ABI, "OrderCreated") if err != nil { - logger.Errorf("IndexOrderCreatedTron.UnpackEventData: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to unpack event data for %s", order.Edges.Token.Edges.Network.Identifier) return err } @@ -526,7 +606,10 @@ func (s *IndexerService) IndexOrderCreatedTron(ctx context.Context, order *ent.P err = s.CreateLockPaymentOrder(ctx, nil, order.Edges.Token.Edges.Network, event) if err != nil { - logger.Errorf("IndexOrderCreatedTron.CreateLockPaymentOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": fmt.Sprintf("0x%v", hex.EncodeToString(event.OrderId[:])), + }).Errorf("Failed to create lock payment order when indexing order created events for %s", order.Edges.Token.Edges.Network.Identifier) } break @@ -555,7 +638,9 @@ func (s *IndexerService) IndexOrderSettled(ctx context.Context, client types.RPC // Initialize contract filterer filterer, err := contracts.NewGatewayFilterer(common.HexToAddress(network.GatewayContractAddress), client) if err != nil { - logger.Errorf("IndexOrderSettled.NewGatewayFilterer: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to filterer when indexing order settled events for %s", network.Identifier) return err } @@ -563,7 +648,9 @@ func (s *IndexerService) IndexOrderSettled(ctx context.Context, client types.RPC header, err := client.HeaderByNumber(ctx, nil) if err != nil { if err != context.Canceled { - logger.Errorf("IndexOrderSettled.HeaderByNumber: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to fetch header by number when indexing order created events for %s", network.Identifier) } return err } @@ -576,7 +663,11 @@ func (s *IndexerService) IndexOrderSettled(ctx context.Context, client types.RPC End: &toBlock, }, nil, nil) if err != nil { - logger.Errorf("IndexOrderSettled.FilterOrderSettled: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Start": uint64(int64(toBlock) - 5000), + "End": toBlock, + }).Errorf("Failed to filter order settled events for %s when indexing order settled events", network.Identifier) return err } @@ -593,7 +684,10 @@ func (s *IndexerService) IndexOrderSettled(ctx context.Context, client types.RPC err := s.UpdateOrderStatusSettled(ctx, network, settledEvent) if err != nil { - logger.Errorf("IndexOrderSettled.UpdateOrderStatusSettled: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": fmt.Sprintf("0x%v", hex.EncodeToString(settledEvent.OrderId[:])), + }).Errorf("Failed to update order status settlement when indexing order settled events for %s", network.Identifier) continue } } @@ -624,17 +718,25 @@ func (s *IndexerService) IndexOrderSettledTron(ctx context.Context, order *ent.L Retry().Set(3, 1*time.Second). Send() if err != nil { - logger.Errorf("fetch txn event logs: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "TxHash": order.TxHash, + }).Errorf("Failed to fetch trx info by id for %s", order.Edges.Token.Edges.Network.Identifier) return err } data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("failed to parse JSON response: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to parse JSON response for %s", order.Edges.Token.Edges.Network.Identifier) return err } - logger.Errorf("IndexOrderSettledTron.gettransactioninfobyid: %v", data) + logger.WithFields(logger.Fields{ + "TxHash": order.TxHash, + "Data": data, + }).Infof("Index Order settlment for %s", order.Edges.Token.Edges.Network.Identifier) // Parse event data for _, event := range data["log"].([]interface{}) { @@ -642,7 +744,10 @@ func (s *IndexerService) IndexOrderSettledTron(ctx context.Context, order *ent.L if eventData["topics"].([]interface{})[0] == "98ece21e01a01cbe1d1c0dad3b053c8fbd368f99be78be958fcf1d1d13fd249a" { unpackedEventData, err := utils.UnpackEventData(eventData["data"].(string), contracts.GatewayMetaData.ABI, "OrderSettled") if err != nil { - logger.Errorf("IndexOrderSettledTron.UnpackEventData: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + }).Errorf("Failed to unpack event data for %s", order.Edges.Token.Edges.Network.Identifier) return err } @@ -657,7 +762,10 @@ func (s *IndexerService) IndexOrderSettledTron(ctx context.Context, order *ent.L err = s.UpdateOrderStatusSettled(ctx, order.Edges.Token.Edges.Network, event) if err != nil { - logger.Errorf("IndexOrderSettledTron.UpdateOrderStatusSettled: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + }).Errorf("Failed to update order status settlement when indexing order settled events for %s", order.Edges.Token.Edges.Network.Identifier) } break @@ -686,7 +794,9 @@ func (s *IndexerService) IndexOrderRefunded(ctx context.Context, client types.RP // Initialize contract filterer filterer, err := contracts.NewGatewayFilterer(common.HexToAddress(network.GatewayContractAddress), client) if err != nil { - logger.Errorf("IndexOrderRefunded.NewGatewayFilterer: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to filter when indexing order refunded events for %s", network.Identifier) return err } @@ -694,7 +804,9 @@ func (s *IndexerService) IndexOrderRefunded(ctx context.Context, client types.RP header, err := client.HeaderByNumber(ctx, nil) if err != nil { if err != context.Canceled { - logger.Errorf("IndexOrderRefunded.HeaderByNumber: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to fetch header by number when indexing order refunded events for %s", network.Identifier) } return err } @@ -707,7 +819,11 @@ func (s *IndexerService) IndexOrderRefunded(ctx context.Context, client types.RP End: &toBlock, }, nil) if err != nil { - logger.Errorf("IndexOrderRefunded.FilterOrderRefunded: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Start": uint64(int64(toBlock) - 5000), + "End": toBlock, + }).Errorf("Failed to filter order refunded events for %s when indexing order refunded events", network.Identifier) return err } @@ -722,7 +838,11 @@ func (s *IndexerService) IndexOrderRefunded(ctx context.Context, client types.RP err := s.UpdateOrderStatusRefunded(ctx, network, refundedEvent) if err != nil { - logger.Errorf("IndexOrderRefunded.UpdateOrderStatusRefunded: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": fmt.Sprintf("0x%v", hex.EncodeToString(refundedEvent.OrderId[:])), + "TxHash": refundedEvent.TxHash, + }).Errorf("Failed to update order status refund when indexing order refunded events for %s", network.Identifier) continue } } @@ -753,17 +873,26 @@ func (s *IndexerService) IndexOrderRefundedTron(ctx context.Context, order *ent. Retry().Set(3, 1*time.Second). Send() if err != nil { - logger.Errorf("fetch txn event logs: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "TxHash": order.TxHash, + }).Errorf("Failed to fetch event logs for %s", order.Edges.Token.Edges.Network.Identifier) return err } data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("failed to parse JSON response: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Response": data, + }).Errorf("Failed to parse JSON response for %s after fetching event logs", order.Edges.Token.Edges.Network.Identifier) return err } - logger.Errorf("IndexOrderRefundedTron.gettransactioninfobyid: %v", data) + logger.WithFields(logger.Fields{ + "TxHash": order.TxHash, + "Data": data, + }).Infof("Index Order refund for %s", order.Edges.Token.Edges.Network.Identifier) // Parse event data for _, event := range data["log"].([]interface{}) { @@ -771,7 +900,9 @@ func (s *IndexerService) IndexOrderRefundedTron(ctx context.Context, order *ent. if eventData["topics"].([]interface{})[0] == "0736fe428e1747ca8d387c2e6fa1a31a0cde62d3a167c40a46ade59a3cdc828e" { unpackedEventData, err := utils.UnpackEventData(eventData["data"].(string), contracts.GatewayMetaData.ABI, "OrderRefunded") if err != nil { - logger.Errorf("IndexOrderRefundedTron.UnpackEventData: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + }).Errorf("Failed to unpack event data for %s after fetching event logs", order.Edges.Token.Edges.Network.Identifier) return err } @@ -784,7 +915,11 @@ func (s *IndexerService) IndexOrderRefundedTron(ctx context.Context, order *ent. err = s.UpdateOrderStatusRefunded(ctx, order.Edges.Token.Edges.Network, event) if err != nil { - logger.Errorf("IndexOrderRefundedTron.UpdateOrderStatusRefunded: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": fmt.Sprintf("0x%v", hex.EncodeToString(event.OrderId[:])), + "TxHash": event.TxHash, + }).Errorf("Failed to update order status refund when indexing order refunded events for %s", order.Edges.Token.Edges.Network.Identifier) } break @@ -853,7 +988,7 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type lockpaymentorder.GatewayIDEQ(gatewayId), ), lockpaymentorder.HasTokenWith( - token.HasNetworkWith( + tokenEnt.HasNetworkWith( networkent.IdentifierEQ(network.Identifier), ), ), @@ -915,7 +1050,7 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type WithNetwork(). Only(ctx) if err != nil { - return fmt.Errorf("failed to fetch token: %w", err) + return nil } // Get order recipient from message hash @@ -926,7 +1061,7 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type // Get provision bucket amountInDecimals := utils.FromSubunit(event.Amount, token.Decimals) - institution, err := utils.GetInstitutionByCode(ctx, recipient.Institution) + institution, err := utils.GetInstitutionByCode(ctx, recipient.Institution, true) if err != nil { return nil } @@ -944,9 +1079,13 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type rate := decimal.NewFromBigInt(event.Rate, -2) - provisionBucket, err := s.getProvisionBucket(ctx, amountInDecimals.Mul(rate), currency) + provisionBucket, isLessThanMin, err := s.getProvisionBucket(ctx, amountInDecimals.Mul(rate), currency) if err != nil { - logger.Errorf("failed to fetch provision bucket: %s %s %v", amountInDecimals, currency, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Amount": amountInDecimals, + "Currency": currency, + }).Errorf("failed to fetch provision bucket when creating lock payment order") } // Create lock payment order fields @@ -963,9 +1102,18 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type AccountName: recipient.AccountName, ProviderID: recipient.ProviderID, Memo: recipient.Memo, + Metadata: recipient.Metadata, ProvisionBucket: provisionBucket, } + if isLessThanMin { + err := s.handleCancellation(ctx, client, nil, &lockPaymentOrder, "Amount is less than the minimum bucket") + if err != nil { + return fmt.Errorf("failed to handle cancellation: %w", err) + } + return nil + } + // Handle private order checks isPrivate := false if lockPaymentOrder.ProviderID != "" { @@ -981,6 +1129,7 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type providerordertoken.HasCurrencyWith( fiatcurrency.CodeEQ(institution.Edges.FiatCurrency.Code), ), + providerordertoken.AddressNEQ(""), ). WithProvider(). Only(ctx) @@ -991,6 +1140,7 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type // 2. Provider does not support the token // 3. Provider does not support the network // 4. Provider does not support the currency + // 5. Provider have not configured a settlement address for the network _ = s.handleCancellation(ctx, client, nil, &lockPaymentOrder, "Provider not available") return nil } else { @@ -1025,13 +1175,20 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type } if provisionBucket == nil && !isPrivate { + // TODO: Activate this when split order is tested and working // Split lock payment order into multiple orders - err = s.splitLockPaymentOrder( - ctx, client, lockPaymentOrder, currency, - ) + // err = s.splitLockPaymentOrder( + // ctx, client, lockPaymentOrder, currency, + // ) + // if err != nil { + // return fmt.Errorf("%s - failed to split lock payment order: %w", lockPaymentOrder.GatewayID, err) + // } + + err = s.handleCancellation(ctx, client, nil, &lockPaymentOrder, "Amount is larger than the maximum bucket") if err != nil { - return fmt.Errorf("%s - failed to split lock payment order: %w", lockPaymentOrder.GatewayID, err) + return fmt.Errorf("failed to handle cancellation: %w", err) } + return nil } else { // Create LockPaymentOrder and recipient in a transaction tx, err := db.Client.Tx(ctx) @@ -1065,8 +1222,9 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type "Amount": lockPaymentOrder.Amount, "Rate": lockPaymentOrder.Rate, "Memo": lockPaymentOrder.Memo, - "ProvisionBucket": lockPaymentOrder.ProvisionBucket, + "Metadata": lockPaymentOrder.Metadata, "ProviderID": lockPaymentOrder.ProviderID, + "ProvisionBucket": lockPaymentOrder.ProvisionBucket, }). Save(ctx) if err != nil { @@ -1089,6 +1247,7 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type SetAccountIdentifier(lockPaymentOrder.AccountIdentifier). SetAccountName(lockPaymentOrder.AccountName). SetMemo(lockPaymentOrder.Memo). + SetMetadata(lockPaymentOrder.Metadata). SetProvisionBucket(lockPaymentOrder.ProvisionBucket) if lockPaymentOrder.ProviderID != "" { @@ -1113,7 +1272,11 @@ func (s *IndexerService) CreateLockPaymentOrder(ctx context.Context, client type if serverConf.Environment == "production" && !strings.HasPrefix(network.Identifier, "tron") { ok, err := s.checkAMLCompliance(network.RPCEndpoint, event.TxHash) if err != nil { - logger.Errorf("checkAMLCompliance: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "endpoint": network.RPCEndpoint, + "TxHash": event.TxHash, + }).Errorf("Failed to check AML Compliance") } if !ok && err == nil { @@ -1154,6 +1317,7 @@ func (s *IndexerService) handleCancellation(ctx context.Context, client types.RP SetAccountIdentifier(lockPaymentOrder.AccountIdentifier). SetAccountName(lockPaymentOrder.AccountName). SetMemo(lockPaymentOrder.Memo). + SetMetadata(lockPaymentOrder.Metadata). SetProvisionBucket(lockPaymentOrder.ProvisionBucket). SetCancellationCount(3). SetCancellationReasons([]string{cancellationReason}). @@ -1176,7 +1340,12 @@ func (s *IndexerService) handleCancellation(ctx context.Context, client types.RP err = s.order.RefundOrder(ctx, client, network, lockPaymentOrder.GatewayID) if err != nil { - logger.Errorf("handleCancellation.RefundOrder(%v): %v", order.ID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "OrderTrxHash": order.TxHash, + "GatewayID": order.GatewayID, + }).Errorf("Handle cancellation failed to refund order") } } else if createdLockPaymentOrder != nil { @@ -1200,7 +1369,12 @@ func (s *IndexerService) handleCancellation(ctx context.Context, client types.RP err = s.order.RefundOrder(ctx, client, network, createdLockPaymentOrder.GatewayID) if err != nil { - logger.Errorf("handleCancellation.RefundOrder(%v): %v", createdLockPaymentOrder.ID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": fmt.Sprintf("0x%v", hex.EncodeToString(createdLockPaymentOrder.ID[:])), + "OrderTrxHash": createdLockPaymentOrder.TxHash, + "GatewayID": createdLockPaymentOrder.GatewayID, + }).Errorf("Handle cancellation failed to refund order") } } @@ -1218,7 +1392,7 @@ func (s *IndexerService) UpdateOrderStatusRefunded(ctx context.Context, network Where( paymentorder.GatewayIDEQ(gatewayId), paymentorder.HasTokenWith( - token.HasNetworkWith( + tokenEnt.HasNetworkWith( networkent.IdentifierEQ(network.Identifier), ), ), @@ -1285,7 +1459,7 @@ func (s *IndexerService) UpdateOrderStatusRefunded(ctx context.Context, network Where( lockpaymentorder.GatewayIDEQ(gatewayId), lockpaymentorder.HasTokenWith( - token.HasNetworkWith( + tokenEnt.HasNetworkWith( networkent.IdentifierEQ(network.Identifier), ), ), @@ -1310,7 +1484,7 @@ func (s *IndexerService) UpdateOrderStatusRefunded(ctx context.Context, network Where( paymentorder.GatewayIDEQ(gatewayId), paymentorder.HasTokenWith( - token.HasNetworkWith( + tokenEnt.HasNetworkWith( networkent.IdentifierEQ(network.Identifier), ), ), @@ -1362,7 +1536,7 @@ func (s *IndexerService) UpdateOrderStatusSettled(ctx context.Context, network * Where( paymentorder.GatewayIDEQ(gatewayId), paymentorder.HasTokenWith( - token.HasNetworkWith( + tokenEnt.HasNetworkWith( networkent.IdentifierEQ(network.Identifier), ), ), @@ -1427,7 +1601,7 @@ func (s *IndexerService) UpdateOrderStatusSettled(ctx context.Context, network * Where( lockpaymentorder.IDEQ(splitOrderId), lockpaymentorder.HasTokenWith( - token.HasNetworkWith( + tokenEnt.HasNetworkWith( networkent.IdentifierEQ(network.Identifier), ), ), @@ -1593,7 +1767,7 @@ func (s *IndexerService) UpdateReceiveAddressStatus( return true, fmt.Errorf("UpdateReceiveAddressStatus.db: %v", err) } - institution, err := utils.GetInstitutionByCode(ctx, orderRecipient.Institution) + institution, err := utils.GetInstitutionByCode(ctx, orderRecipient.Institution, true) if err != nil { return true, fmt.Errorf("UpdateReceiveAddressStatus.db: %v", err) } @@ -1688,13 +1862,19 @@ func (s *IndexerService) fetchLatestOrderEvents(rpcEndpoint, network, txHash str Retry().Set(3, 1*time.Second). Send() if err != nil { - logger.Errorf("fetch txn event logs: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "TxHash": txHash, + }).Errorf("Failed to fetch txn event logs for %s", network) return nil, err } data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("failed to parse JSON response: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Response": data, + }).Errorf("Failed to parse JSON response for %s after fetching event logs", network) return nil, err } @@ -1706,7 +1886,7 @@ func (s *IndexerService) fetchLatestOrderEvents(rpcEndpoint, network, txHash str } // getProvisionBucket returns the provision bucket for a lock payment order -func (s *IndexerService) getProvisionBucket(ctx context.Context, amount decimal.Decimal, currency *ent.FiatCurrency) (*ent.ProvisionBucket, error) { +func (s *IndexerService) getProvisionBucket(ctx context.Context, amount decimal.Decimal, currency *ent.FiatCurrency) (*ent.ProvisionBucket, bool, error) { provisionBucket, err := db.Client.ProvisionBucket. Query(). Where( @@ -1719,10 +1899,28 @@ func (s *IndexerService) getProvisionBucket(ctx context.Context, amount decimal. WithCurrency(). Only(ctx) if err != nil { - return nil, fmt.Errorf("failed to fetch provision bucket: %w", err) + if ent.IsNotFound(err) { + // Check if the amount is less than the minimum bucket + minBucket, err := db.Client.ProvisionBucket. + Query(). + Where( + provisionbucket.HasCurrencyWith( + fiatcurrency.IDEQ(currency.ID), + ), + ). + Order(ent.Asc(provisionbucket.FieldMinAmount)). + First(ctx) + if err != nil { + return nil, false, fmt.Errorf("failed to fetch minimum bucket: %w", err) + } + if amount.LessThan(minBucket.MinAmount) { + return nil, true, nil + } + } + return nil, false, fmt.Errorf("failed to fetch provision bucket: %w", err) } - return provisionBucket, nil + return provisionBucket, false, nil } // splitLockPaymentOrder splits a lock payment order into multiple orders @@ -1736,7 +1934,12 @@ func (s *IndexerService) splitLockPaymentOrder(ctx context.Context, client types Order(ent.Desc(provisionbucket.FieldMaxAmount)). All(ctx) if err != nil { - logger.Errorf("failed to fetch provision buckets: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Currency": currency.ID, + "CurrencyCode": currency.Code, + "CurrencEnabled": currency.IsEnabled, + }).Errorf("failed to fetch provision buckets when splitting lock payment order") return err } @@ -1792,14 +1995,20 @@ func (s *IndexerService) splitLockPaymentOrder(ctx context.Context, client types CreateBulk(lockOrders...). Save(ctx) if err != nil { - logger.Errorf("failed to create lock payment orders in bulk: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": lockPaymentOrder.GatewayID, + }).Errorf("Failed to create lock payment order in Bulk when splitting lock payment order") _ = tx.Rollback() return err } // Commit the transaction if everything succeeded if err := tx.Commit(); err != nil { - logger.Errorf("failed to split lock payment order: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": lockPaymentOrder.GatewayID, + }).Errorf("Failed to commit transaction when splitting lock payment order") return err } @@ -1807,14 +2016,23 @@ func (s *IndexerService) splitLockPaymentOrder(ctx context.Context, client types if serverConf.Environment == "production" && !strings.HasPrefix(lockPaymentOrder.Network.Identifier, "tron") { ok, err := s.checkAMLCompliance(lockPaymentOrder.Network.RPCEndpoint, lockPaymentOrder.TxHash) if err != nil { - logger.Errorf("splitLockPaymentOrder.checkAMLCompliance: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "endpoint": lockPaymentOrder.Network.RPCEndpoint, + "TxHash": lockPaymentOrder.TxHash, + }).Errorf("Failed to check AML Compliance") } if !ok && err == nil && len(ordersCreated) > 0 { isRefunded = true err := s.handleCancellation(ctx, client, ordersCreated[0], nil, "AML compliance check failed") if err != nil { - logger.Errorf("splitLockPaymentOrder.checkAMLCompliance.RefundOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": ordersCreated[0].ID, + "OrderGatewayID": ordersCreated[0].GatewayID, + "Reason": "AML compliance check failed", + }).Errorf("Failed to handle cancellation when splitting lock payment order") } break } @@ -1832,7 +2050,7 @@ func (s *IndexerService) splitLockPaymentOrder(ctx context.Context, client types largestBucket := buckets[0] if amountToSplit.LessThan(largestBucket.MaxAmount) { - bucket, err := s.getProvisionBucket(ctx, amountToSplit, currency) + bucket, _, err := s.getProvisionBucket(ctx, amountToSplit, currency) if err != nil { return err } @@ -1857,7 +2075,10 @@ func (s *IndexerService) splitLockPaymentOrder(ctx context.Context, client types orderCreated, err := orderCreatedUpdate.Save(ctx) if err != nil { - logger.Errorf("failed to create lock payment order: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": lockPaymentOrder.GatewayID, + }).Errorf("Failed to create lock payment order when splitting lock payment order") return err } diff --git a/services/indexer_test.go b/services/indexer_test.go index 3043911b..c4e34f6b 100644 --- a/services/indexer_test.go +++ b/services/indexer_test.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" _ "github.com/mattn/go-sqlite3" "github.com/paycrest/aggregator/ent" "github.com/paycrest/aggregator/ent/enttest" @@ -281,3 +282,144 @@ func IndexERC20Transfer(ctx context.Context, client types.RPCClient, receiveAddr return nil } + +func TestGetProvisionBucket(t *testing.T) { + ctx := context.Background() + + // Set up test database client + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1") + defer client.Close() + db.Client = client + + // Setup test data (reusing your setup function where possible) + err := setup() + assert.NoError(t, err) + + // Create a test currency (USD) with a UUID and all required fields + usdID := uuid.New() + currencyUSD, err := db.Client.FiatCurrency. + Create(). + SetID(usdID). + SetCode("USD"). + SetShortName("Dollar"). + SetSymbol("$"). + SetName("US Dollar"). + SetMarketRate(decimal.Zero). + Save(ctx) + if err != nil { + t.Fatalf("failed to create USD currency: %v", err) + } + + // Create a test currency (EUR) with a UUID and all required fields + eurID := uuid.New() + currencyEUR, err := db.Client.FiatCurrency. + Create(). + SetID(eurID). + SetCode("EUR"). + SetShortName("Euro"). + SetSymbol("€"). + SetName("Euro"). + SetMarketRate(decimal.Zero). // Required, set to zero if not used + Save(ctx) + if err != nil { + t.Fatalf("failed to create EUR currency: %v", err) + } + + // Create provision buckets for USD + bucket10to100, err := db.Client.ProvisionBucket. + Create(). + SetMinAmount(decimal.NewFromInt(10)). + SetMaxAmount(decimal.NewFromInt(100)). + SetCurrency(currencyUSD). + Save(ctx) + if err != nil { + t.Fatalf("failed to create bucket 10-100: %v", err) + } + + bucket100to1000, err := db.Client.ProvisionBucket. + Create(). + SetMinAmount(decimal.NewFromInt(100)). + SetMaxAmount(decimal.NewFromInt(1000)). + SetCurrency(currencyUSD). + Save(ctx) + if err != nil { + t.Fatalf("failed to create bucket 100-1000: %v", err) + } + + indexer := testCtx.indexer + + tests := []struct { + name string + amount decimal.Decimal + currency *ent.FiatCurrency + wantBucket *ent.ProvisionBucket + wantLessThanMin bool + wantErr bool + errMsg string + }{ + { + name: "matching_bucket_found_10to100", + amount: decimal.NewFromInt(50), + currency: currencyUSD, + wantBucket: bucket10to100, + wantLessThanMin: false, + wantErr: false, + }, + { + name: "matching_bucket_found_100to1000", + amount: decimal.NewFromInt(500), + currency: currencyUSD, + wantBucket: bucket100to1000, + wantLessThanMin: false, + wantErr: false, + }, + { + name: "below_minimum_bucket", + amount: decimal.NewFromInt(5), + currency: currencyUSD, + wantBucket: nil, + wantLessThanMin: true, + wantErr: false, + }, + { + name: "above_maximum_bucket", + amount: decimal.NewFromInt(2000), // Fixed: > 1000 + currency: currencyUSD, + wantBucket: nil, + wantLessThanMin: false, + wantErr: true, + errMsg: "failed to fetch provision bucket: ent: provision_bucket not found", + }, + { + name: "no_buckets_for_currency", + amount: decimal.NewFromInt(50), + currency: currencyEUR, + wantBucket: nil, + wantLessThanMin: false, + wantErr: true, + errMsg: "failed to fetch minimum bucket: ent: provision_bucket not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bucket, isLessThanMin, err := indexer.getProvisionBucket(ctx, tt.amount, tt.currency) + + if tt.wantErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errMsg) + assert.Nil(t, bucket) + assert.Equal(t, tt.wantLessThanMin, isLessThanMin) + return + } + + assert.NoError(t, err) + if tt.wantBucket != nil { + assert.Equal(t, tt.wantBucket.ID, bucket.ID) + } else { + assert.Nil(t, bucket) + } + assert.Equal(t, tt.wantLessThanMin, isLessThanMin) + }) + } +} diff --git a/services/kyc/errors/errors.go b/services/kyc/errors/errors.go new file mode 100644 index 00000000..d1fbefd5 --- /dev/null +++ b/services/kyc/errors/errors.go @@ -0,0 +1,32 @@ +package errors + +import "fmt" + +// Common error types for KYC providers +type ( + ErrSignatureAlreadyUsed struct{} + ErrAlreadyVerified struct{} + ErrProviderUnreachable struct{ Err error } + ErrProviderResponse struct{ Err error } + ErrDatabase struct{ Err error } +) + +func (e ErrSignatureAlreadyUsed) Error() string { + return "signature already used for identity verification" +} + +func (e ErrAlreadyVerified) Error() string { + return "this account has already been successfully verified" +} + +func (e ErrProviderUnreachable) Error() string { + return fmt.Sprintf("failed to request identity verification: couldn't reach identity provider: %v", e.Err) +} + +func (e ErrProviderResponse) Error() string { + return fmt.Sprintf("failed to request identity verification: %v", e.Err) +} + +func (e ErrDatabase) Error() string { + return fmt.Sprintf("database error: %v", e.Err) +} diff --git a/services/kyc/interface.go b/services/kyc/interface.go deleted file mode 100644 index d4e9c31c..00000000 --- a/services/kyc/interface.go +++ /dev/null @@ -1,11 +0,0 @@ -package kyc - -import ( - "context" -) - -type KYCProvider interface { - RequestVerification(ctx context.Context, req NewIDVerificationRequest) (*NewIDVerificationResponse, error) - CheckStatus(ctx context.Context, walletAddress string) (*IDVerificationStatusResponse, error) - HandleWebhook(ctx context.Context, payload []byte) error -} diff --git a/services/kyc/smile.go b/services/kyc/smile.go deleted file mode 100644 index ab5fda09..00000000 --- a/services/kyc/smile.go +++ /dev/null @@ -1,315 +0,0 @@ -package kyc - -import ( - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "slices" - "strings" - "time" - - "github.com/ethereum/go-ethereum/crypto" - fastshot "github.com/opus-domini/fast-shot" - "github.com/paycrest/aggregator/config" - "github.com/paycrest/aggregator/ent" - "github.com/paycrest/aggregator/ent/identityverificationrequest" - "github.com/paycrest/aggregator/storage" - "github.com/paycrest/aggregator/utils" - "github.com/paycrest/aggregator/utils/logger" -) - -type SmileIDService struct { - identityConf *config.IdentityConfiguration - serverConf *config.ServerConfiguration - db *ent.Client -} - -func NewSmileIDService() *SmileIDService { - return &SmileIDService{ - identityConf: config.IdentityConfig(), - serverConf: config.ServerConfig(), - db: storage.Client, - } -} - -func (s *SmileIDService) RequestVerification(ctx context.Context, payload NewIDVerificationRequest) (*NewIDVerificationResponse, error) { - signature, err := hex.DecodeString(payload.Signature) - if err != nil { - return nil, fmt.Errorf("invalid signature: signature is not in the correct format") - } - if len(signature) != 65 { - return nil, fmt.Errorf("invalid signature: signature length is not correct") - } - if signature[64] != 27 && signature[64] != 28 { - return nil, fmt.Errorf("invalid signature: invalid recovery ID") - } - signature[64] -= 27 - - message := fmt.Sprintf("I accept the KYC Policy and hereby request an identity verification check for %s with nonce %s", payload.WalletAddress, payload.Nonce) - prefix := "\x19Ethereum Signed Message:\n" + fmt.Sprint(len(message)) - hash := crypto.Keccak256Hash([]byte(prefix + message)) - - sigPublicKeyECDSA, err := crypto.SigToPub(hash.Bytes(), signature) - if err != nil { - return nil, fmt.Errorf("invalid signature") - } - recoveredAddress := crypto.PubkeyToAddress(*sigPublicKeyECDSA) - if !strings.EqualFold(recoveredAddress.Hex(), payload.WalletAddress) { - return nil, fmt.Errorf("invalid signature") - } - - ivr, err := s.db.IdentityVerificationRequest. - Query(). - Where(identityverificationrequest.WalletAddressEQ(payload.WalletAddress)). - Only(ctx) - if err != nil { - if !ent.IsNotFound(err) { - logger.Errorf("error: %v", err) - return nil, fmt.Errorf("failed to request identity verification") - } - } - - timestamp := time.Now() - - if ivr != nil { - if ivr.WalletSignature == payload.Signature { - return nil, fmt.Errorf("signature already used for identity verification") - } - - expiryPeriod := 15 * time.Minute - - if ivr.Status == identityverificationrequest.StatusFailed || (ivr.Status == identityverificationrequest.StatusPending && ivr.LastURLCreatedAt.Add(expiryPeriod).Before(timestamp)) { - _, err := s.db.IdentityVerificationRequest. - Delete(). - Where(identityverificationrequest.WalletAddressEQ(payload.WalletAddress)). - Exec(ctx) - if err != nil { - logger.Errorf("error: %v", err) - return nil, fmt.Errorf("failed to request identity verification") - } - } else if ivr.Status == identityverificationrequest.StatusPending && (ivr.LastURLCreatedAt.Add(expiryPeriod).Equal(timestamp) || ivr.LastURLCreatedAt.Add(expiryPeriod).After(timestamp)) { - _, err = ivr. - Update(). - SetWalletSignature(payload.Signature). - Save(ctx) - if err != nil { - logger.Errorf("error: %v", err) - return nil, fmt.Errorf("failed to request identity verification") - } - return &NewIDVerificationResponse{ - URL: ivr.VerificationURL, - ExpiresAt: ivr.LastURLCreatedAt, - }, nil - } - - if ivr.Status == identityverificationrequest.StatusSuccess { - return nil, fmt.Errorf("this account has already been successfully verified") - } - } - - smileIDSignature := s.getSmileIDSignature(timestamp.Format(time.RFC3339Nano)) - res, err := fastshot.NewClient(s.identityConf.SmileIdentityBaseUrl). - Config().SetTimeout(30 * time.Second). - Build().POST("/v1/smile_links"). - Body().AsJSON(map[string]interface{}{ - "partner_id": s.identityConf.SmileIdentityPartnerId, - "signature": smileIDSignature, - "timestamp": timestamp, - "name": "Aggregator KYC", - "company_name": "Paycrest", - "id_types": []map[string]interface{}{ - {"country": "NG", "id_type": "NIN_SLIP", "verification_method": "biometric_kyc"}, - {"country": "NG", "id_type": "V_NIN", "verification_method": "biometric_kyc"}, - {"country": "NG", "id_type": "BVN", "verification_method": "biometric_kyc"}, - {"country": "NG", "id_type": "PASSPORT", "verification_method": "doc_verification"}, - {"country": "NG", "id_type": "DRIVERS_LICENSE", "verification_method": "doc_verification"}, - {"country": "NG", "id_type": "RESIDENT_ID", "verification_method": "doc_verification"}, - {"country": "NG", "id_type": "IDENTITY_CARD", "verification_method": "doc_verification"}, - {"country": "NG", "id_type": "TRAVEL_DOC", "verification_method": "doc_verification"}, - {"country": "KE", "id_type": "PASSPORT", "verification_method": "doc_verification"}, - {"country": "KE", "id_type": "DRIVERS_LICENSE", "verification_method": "doc_verification"}, - {"country": "KE", "id_type": "ALIEN_CARD", "verification_method": "doc_verification"}, - {"country": "KE", "id_type": "NATIONAL_ID", "verification_method": "doc_verification"}, - {"country": "KE", "id_type": "RESIDENT_ID", "verification_method": "doc_verification"}, - {"country": "KE", "id_type": "TRAVEL_DOC", "verification_method": "doc_verification"}, - {"country": "GH", "id_type": "IDENTITY_CARD", "verification_method": "doc_verification"}, - {"country": "GH", "id_type": "PASSPORT", "verification_method": "doc_verification"}, - {"country": "GH", "id_type": "VOTER_ID", "verification_method": "doc_verification"}, - // {"country": "GH", "id_type": "NEW_VOTER_ID", "verification_method": "biometric_kyc"}, - {"country": "GH", "id_type": "DRIVERS_LICENSE", "verification_method": "doc_verification"}, - // {"country": "GH", "id_type": "SSNIT", "verification_method": "biometric_kyc"}, - {"country": "GH", "id_type": "RESIDENT_ID", "verification_method": "doc_verification"}, - {"country": "GH", "id_type": "TRAVEL_DOC", "verification_method": "doc_verification"}, - {"country": "ZA", "id_type": "PASSPORT", "verification_method": "doc_verification"}, - {"country": "ZA", "id_type": "DRIVERS_LICENSE", "verification_method": "doc_verification"}, - {"country": "ZA", "id_type": "RESIDENT_ID", "verification_method": "doc_verification"}, - {"country": "ZA", "id_type": "IDENTITY_CARD", "verification_method": "doc_verification"}, - {"country": "ZA", "id_type": "NATIONAL_ID", "verification_method": "biometric_kyc"}, - {"country": "ZA", "id_type": "TRAVEL_DOC", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "CITIZEN_ID", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "DRIVERS_LICENSE", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "HEALTH_CARD", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "IDENTITY_CARD", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "PASSPORT", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "RESIDENT_ID", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "SEAMANS_ID", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "TRAVEL_DOC", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification"}, - {"country": "CA", "id_type": "WORK_PERMIT", "verification_method": "doc_verification"}, - }, - "callback_url": fmt.Sprintf("%s/v1/kyc/webhook", s.serverConf.HostDomain), - "data_privacy_policy_url": "https://paycrest.notion.site/KYC-Policy-10e2482d45a280e191b8d47d76a8d242", - "logo_url": "https://res.cloudinary.com/de6e0wihu/image/upload/v1738088043/xxhlrsld2wy9lzekahur.png", - "is_single_use": true, - "user_id": payload.WalletAddress, - "expires_at": timestamp.Add(1 * time.Hour).Format(time.RFC3339Nano), - }). - Send() - if err != nil { - logger.Errorf("error: %v", err) - return nil, fmt.Errorf("failed to request identity verification: couldn't reach identity provider") - } - - data, err := utils.ParseJSONResponse(res.RawResponse) - if err != nil { - logger.Errorf("error: %v %v", err, data) - return nil, fmt.Errorf("failed to request identity verification: %v", data) - } - - ivr, err = s.db.IdentityVerificationRequest. - Create(). - SetWalletAddress(payload.WalletAddress). - SetWalletSignature(payload.Signature). - SetPlatform("smile_id"). - SetPlatformRef(data["ref_id"].(string)). - SetVerificationURL(data["link"].(string)). - SetLastURLCreatedAt(timestamp). - Save(ctx) - if err != nil { - logger.Errorf("error: %v", err) - return nil, fmt.Errorf("failed to request identity verification") - } - - return &NewIDVerificationResponse{ - URL: ivr.VerificationURL, - ExpiresAt: ivr.LastURLCreatedAt, - }, nil -} - -func (s *SmileIDService) CheckStatus(ctx context.Context, walletAddress string) (*IDVerificationStatusResponse, error) { - - ivr, err := s.db.IdentityVerificationRequest. - Query(). - Where(identityverificationrequest.WalletAddressEQ(walletAddress)). - Only(ctx) - if err != nil { - if ent.IsNotFound(err) { - // Check the platform's status endpoint - return nil, fmt.Errorf("no verification request found for this wallet address") - } - logger.Errorf("error: %v", err) - return nil, fmt.Errorf("failed to fetch identity verification status") - } - - response := &IDVerificationStatusResponse{ - URL: ivr.VerificationURL, - Status: ivr.Status.String(), - } - - // Check if the verification URL has expired - if ivr.LastURLCreatedAt.Add(1*time.Hour).Before(time.Now()) && ivr.Status == identityverificationrequest.StatusPending { - response.Status = "expired" - } - - return response, nil -} - -func (s *SmileIDService) HandleWebhook(ctx context.Context, payload []byte) error { - var smilePayload SmileIDWebhookPayload - - // Parse the JSON payload - if err := json.Unmarshal(payload, &smilePayload); err != nil { - logger.Errorf("failed to parse webhook payload: %v", err) - return fmt.Errorf("invalid payload") - } - - if !s.verifySmileIDWebhookSignature(smilePayload, smilePayload.Signature) { - logger.Errorf("invalid webhook signature") - return fmt.Errorf("invalid signature") - } - - // Process the webhook - status := identityverificationrequest.StatusPending - - // Check for success codes - successCodes := []string{ - "0810", // Document Verified - "1020", // Exact Match (Basic KYC and Enhanced KYC) - "1012", // Valid ID / ID Number Validated (Enhanced KYC) - "0820", // Authenticate User Machine Judgement - PASS - "0840", // Enroll User PASS - Machine Judgement - } - - // Check for failed codes - failedCodes := []string{ - "0811", // No Face Match - "0812", // Filed Security Features Check - "0813", // Document Not Verified - Machine Judgement - "1022", // No Match - "1023", // No Found - "1011", // Invalid ID / ID Number Invalid - "1013", // ID Number Not Found - "1014", // Unsupported ID Type - "0821", // Images did not match - "0911", // No Face Found - "0912", // Face Not Matching - "0921", // Face Not Found - "0922", // Selfie Quality Too Poor - "0841", // Enroll User FAIL - "0941", // Face Not Found - "0942", // Face Poor Quality - } - - if slices.Contains(successCodes, smilePayload.ResultCode) { - status = identityverificationrequest.StatusSuccess - } - if slices.Contains(failedCodes, smilePayload.ResultCode) { - status = identityverificationrequest.StatusFailed - } - - // Update the verification status in the database - _, err := s.db.IdentityVerificationRequest. - Update(). - Where( - identityverificationrequest.WalletAddressEQ(smilePayload.PartnerParams.UserID), - identityverificationrequest.StatusEQ(identityverificationrequest.StatusPending), - ). - SetStatus(status). - Save(ctx) - if err != nil { - logger.Errorf("failed to update verification status: %v", err) - return fmt.Errorf("failed to process webhook") - } - - return nil -} - -// verifyWebhookSignature verifies the signature of a Smile Identity webhook -func (s *SmileIDService) verifySmileIDWebhookSignature(payload SmileIDWebhookPayload, receivedSignature string) bool { - computedSignature := s.getSmileIDSignature(payload.Timestamp) - return computedSignature == receivedSignature -} - -// getSmileIDSignature generates a signature for a Smile ID request -func (s *SmileIDService) getSmileIDSignature(timestamp string) string { - h := hmac.New(sha256.New, []byte(s.identityConf.SmileIdentityApiKey)) - h.Write([]byte(timestamp)) - h.Write([]byte(s.identityConf.SmileIdentityPartnerId)) - h.Write([]byte("sid_request")) - return base64.StdEncoding.EncodeToString(h.Sum(nil)) -} diff --git a/services/kyc/smile/id_types.json b/services/kyc/smile/id_types.json new file mode 100644 index 00000000..22449a59 --- /dev/null +++ b/services/kyc/smile/id_types.json @@ -0,0 +1,2734 @@ +{ + "continents": [ + { + "name": "Africa", + "countries": [ + { + "name": "Algeria", + "code": "DZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Angola", + "code": "AO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Benin", + "code": "BJ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Botswana", + "code": "BW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Burkina Faso", + "code": "BF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Burundi", + "code": "BI", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cameroon", + "code": "CM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cape Verde", + "code": "CV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Central African Republic", + "code": "CF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Chad", + "code": "TD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Comoros", + "code": "KM", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Congo", + "code": "CG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Congo, Democratic Republic of the", + "code": "CD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cote d'Ivoire", + "code": "CI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Djibouti", + "code": "DJ", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Egypt", + "code": "EG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Equatorial Guinea", + "code": "GQ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Eritrea", + "code": "ER", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Eswatini", + "code": "SZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ethiopia", + "code": "ET", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Gabon", + "code": "GA", + "id_types": [ + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Gambia", + "code": "GM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ghana", + "code": "GH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guinea", + "code": "GN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guinea-Bissau", + "code": "GW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kenya", + "code": "KE", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "NATIONAL_ID", "verification_method": "doc_verification"}, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lesotho", + "code": "LS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Liberia", + "code": "LR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Libya", + "code": "LY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Madagascar", + "code": "MG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Malawi", + "code": "MW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mali", + "code": "ML", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mauritania", + "code": "MR", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mauritius", + "code": "MU", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Morocco", + "code": "MA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mozambique", + "code": "MZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Namibia", + "code": "NA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Niger", + "code": "NE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nigeria", + "code": "NG", + "id_types": [ + { "type": "BVN", "verification_method": "biometric_kyc" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "NIN_SLIP", "verification_method": "biometric_kyc" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "V_NIN", "verification_method": "biometric_kyc" } + ] + }, + { + "name": "Rwanda", + "code": "RW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Helena, Ascension and Tristan da Cunha", + "code": "SH", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sao Tome and Principe", + "code": "ST", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Senegal", + "code": "SN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Seychelles", + "code": "SC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sierra Leone", + "code": "SL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Somalia", + "code": "SO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "South Africa", + "code": "ZA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "South Sudan", + "code": "SS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sudan", + "code": "SD", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tanzania, United Republic of", + "code": "TZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Togo", + "code": "TG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tunisia", + "code": "TN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Uganda", + "code": "UG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Western Sahara", + "code": "EH", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Zambia", + "code": "ZM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Zimbabwe", + "code": "ZW", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "Asia and the Middle East", + "countries": [ + { + "name": "Afghanistan", + "code": "AF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Armenia", + "code": "AM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Azerbaijan", + "code": "AZ", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bahrain", + "code": "BH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bangladesh", + "code": "BD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bhutan", + "code": "BT", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Brunei Darussalam", + "code": "BN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cambodia", + "code": "KH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "China", + "code": "CN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cyprus", + "code": "CY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Georgia", + "code": "GE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Hong Kong", + "code": "HK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "India", + "code": "IN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Indonesia", + "code": "ID", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Iran, Islamic Republic of", + "code": "IR", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Iraq", + "code": "IQ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Israel", + "code": "IL", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Japan", + "code": "JP", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Jordan", + "code": "JO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kazakhstan", + "code": "KZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Korea, Democratic People's Republic of", + "code": "KP", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Korea, Republic of", + "code": "KR", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kuwait", + "code": "KW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kyrgyzstan", + "code": "KG", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lao People's Democratic Republic", + "code": "LA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lebanon", + "code": "LB", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Macao", + "code": "MO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Malaysia", + "code": "MY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Maldives", + "code": "MV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mongolia", + "code": "MN", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Myanmar", + "code": "MM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nepal", + "code": "NP", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Oman", + "code": "OM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Pakistan", + "code": "PK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Palestine, State of", + "code": "PS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Philippines", + "code": "PH", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Qatar", + "code": "QA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saudi Arabia", + "code": "SA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Singapore", + "code": "SG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sri Lanka", + "code": "LK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Syrian Arab Republic", + "code": "SY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Taiwan, Province of China", + "code": "TW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tajikistan", + "code": "TJ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Thailand", + "code": "TH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Timor-Leste", + "code": "TL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Turkey", + "code": "TR", + "id_types": [ + { "type": "ADDRESS_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Turkmenistan", + "code": "TM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "United Arab Emirates", + "code": "AE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Uzbekistan", + "code": "UZ", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Viet Nam", + "code": "VN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Yemen", + "code": "YE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "Europe", + "countries": [ + { + "name": "Albania", + "code": "AL", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Andorra", + "code": "AD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Austria", + "code": "AT", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Belarus", + "code": "BY", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Belgium", + "code": "BE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bosnia and Herzegovina", + "code": "BA", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bulgaria", + "code": "BG", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Croatia", + "code": "HR", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Czech Republic", + "code": "CZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Denmark", + "code": "DK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Estonia", + "code": "EE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Faroe Islands", + "code": "FO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Finland", + "code": "FI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "France", + "code": "FR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Germany", + "code": "DE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Gibraltar", + "code": "GI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Greece", + "code": "GR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guernsey", + "code": "GG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Holy See (Vatican City State)", + "code": "VA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Hungary", + "code": "HU", + "id_types": [ + { "type": "ADDRESS_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Iceland", + "code": "IS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ireland", + "code": "IE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Isle of Man", + "code": "IM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Italy", + "code": "IT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Jersey", + "code": "JE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Latvia", + "code": "LV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Liechtenstein", + "code": "LI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lithuania", + "code": "LT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Luxembourg", + "code": "LU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Malta", + "code": "MT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Moldova, Republic of", + "code": "MD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Monaco", + "code": "MC", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Montenegro", + "code": "ME", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Netherlands", + "code": "NL", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "North Macedonia, Republic of", + "code": "MK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Norway", + "code": "NO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Poland", + "code": "PL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VEHICLE_PARTICULARS", "verification_method": "doc_verification" } + ] + }, + { + "name": "Portugal", + "code": "PT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Romania", + "code": "RO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Russian Federation", + "code": "RU", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VEHICLE_PARTICULARS", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "San Marino", + "code": "SM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Serbia", + "code": "RS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Slovakia", + "code": "SK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Slovenia", + "code": "SI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Spain", + "code": "ES", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sweden", + "code": "SE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Switzerland", + "code": "CH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ukraine", + "code": "UA", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "United Kingdom", + "code": "GB", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "North America", + "countries": [ + { + "name": "Anguilla", + "code": "AI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Antigua and Barbuda", + "code": "AG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Aruba", + "code": "AW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bahamas", + "code": "BS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Barbados", + "code": "BB", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Belize", + "code": "BZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bermuda", + "code": "BM", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bonaire, Sint Eustatius and Saba", + "code": "BQ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Canada", + "code": "CA", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cayman Islands", + "code": "KY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Costa Rica", + "code": "CR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cuba", + "code": "CU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Curacao", + "code": "CW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Dominica", + "code": "DM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Dominican Republic", + "code": "DO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "El Salvador", + "code": "SV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Greenland", + "code": "GL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Grenada", + "code": "GD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guatemala", + "code": "GT", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Haiti", + "code": "HT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Honduras", + "code": "HN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Jamaica", + "code": "JM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mexico", + "code": "MX", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Montserrat", + "code": "MS", + "id_types": [ + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nicaragua", + "code": "NI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Panama", + "code": "PA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Puerto Rico", + "code": "PR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Kitts and Nevis", + "code": "KN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Lucia", + "code": "LC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Martin (French part)", + "code": "MF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Vincent and the Grenadines", + "code": "VC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sint Maarten (Dutch part)", + "code": "SX", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Trinidad and Tobago", + "code": "TT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Turks and Caicos Islands", + "code": "TC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "United States", + "code": "US", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "TRIBAL_CARD", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Virgin Islands, British", + "code": "VG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Virgin Islands, U.S.", + "code": "VI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "Oceania", + "countries": [ + { + "name": "American Samoa", + "code": "AS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Australia", + "code": "AU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cook Islands", + "code": "CK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Fiji", + "code": "FJ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "French Polynesia", + "code": "PF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guam", + "code": "GU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kiribati", + "code": "KI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Marshall Islands", + "code": "MH", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Micronesia, Federated States of", + "code": "FM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nauru", + "code": "NR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "New Zealand", + "code": "NZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Niue", + "code": "NU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Northern Mariana Islands", + "code": "MP", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Palau", + "code": "PW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Papua New Guinea", + "code": "PG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Samoa", + "code": "WS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Solomon Islands", + "code": "SB", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tonga", + "code": "TO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tuvalu", + "code": "TV", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Vanuatu", + "code": "VU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "South America", + "countries": [ + { + "name": "Argentina", + "code": "AR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VEHICLE_PARTICULARS", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bolivia, Plurinational State of", + "code": "BO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Brazil", + "code": "BR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Chile", + "code": "CL", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Colombia", + "code": "CO", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ecuador", + "code": "EC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guyana", + "code": "GY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Paraguay", + "code": "PY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Peru", + "code": "PE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Suriname", + "code": "SR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Uruguay", + "code": "UY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Venezuela, Bolivarian Republic of", + "code": "VE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + } + ] + } + ] + } + \ No newline at end of file diff --git a/services/kyc/smile/id_types_schema.json b/services/kyc/smile/id_types_schema.json new file mode 100644 index 00000000..6aa2354d --- /dev/null +++ b/services/kyc/smile/id_types_schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "continents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "countries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "code": { "type": "string", "pattern": "^[A-Z]{2}$" }, + "id_types": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string", "minLength": 1 }, + "verification_method": { "type": "string", "enum": ["biometric_kyc", "doc_verification"] } + }, + "required": ["type", "verification_method"], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": ["name", "code", "id_types"], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": ["name", "countries"], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": ["continents"], + "additionalProperties": false +} \ No newline at end of file diff --git a/services/kyc/smile/id_types_test.go b/services/kyc/smile/id_types_test.go new file mode 100644 index 00000000..b87c44d4 --- /dev/null +++ b/services/kyc/smile/id_types_test.go @@ -0,0 +1,440 @@ +package smile + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/xeipuuv/gojsonschema" +) + +var ( + configPath = "./id_types.json" + schemaPath = "./id_types_schema.json" +) + +func ValidateSmileIDConfig(filePath string) error { + // Read the config file + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + // Validate against JSON schema + schemaPath := "./id_types_schema.json" + schemaLoader := gojsonschema.NewReferenceLoader("file://" + schemaPath) + documentLoader := gojsonschema.NewBytesLoader(data) + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return fmt.Errorf("failed to validate schema: %w", err) + } + if !result.Valid() { + var errors []string + for _, e := range result.Errors() { + errors = append(errors, e.String()) + } + return fmt.Errorf("schema validation failed: %v", errors) + } + + // Ensure JSON is parseable into SmileIDConfig + var config SmileIDConfig + if err := json.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse JSON: %w", err) + } + + return nil +} + +// TestValidateSmileIDConfig tests the validation of id_types.json +func TestValidateSmileIDConfig(t *testing.T) { + + // Ensure the real config and schema files exist + if _, err := os.Stat(configPath); os.IsNotExist(err) { + t.Fatalf("Config file %s not found", configPath) + } + if _, err := os.Stat(schemaPath); os.IsNotExist(err) { + t.Fatalf("Schema file %s not found", schemaPath) + } + + // Create a temporary directory for modified configs + tmpDir := t.TempDir() + + var err error + + t.Run("ValidConfig", func(t *testing.T) { + err := ValidateSmileIDConfig(configPath) + if err != nil { + t.Errorf("Expected no error for valid config, got: %v", err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + schemaLoader := gojsonschema.NewReferenceLoader("file://" + schemaPath) + documentLoader := gojsonschema.NewBytesLoader(data) + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + t.Fatalf("Schema validation error: %v", err) + } + if !result.Valid() { + t.Errorf("Schema validation failed: %v", result.Errors()) + } + }) + + t.Run("EmptyContinents", func(t *testing.T) { + invalidConfig := map[string]interface{}{"continents": []interface{}{}} + tmpPath := filepath.Join(tmpDir, "empty_continents.json") + data, _ := json.Marshal(invalidConfig) + os.WriteFile(tmpPath, data, 0644) + + err := ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "continents: Array must have at least 1 items") { + t.Errorf("Expected error containing 'continents: Array must have at least 1 items', got: %v", err) + } + }) + + t.Run("EmptyContinentName", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + continent := continents[0].(map[string]interface{}) + continent["name"] = "" + tmpPath := filepath.Join(tmpDir, "empty_continent_name.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "name: String length must be greater than or equal to 1") { + t.Errorf("Expected error containing 'name: String length must be greater than or equal to 1', got: %v", err) + } + }) + + t.Run("EmptyCountries", func(t *testing.T) { + invalidConfig := map[string]interface{}{ + "continents": []interface{}{ + map[string]interface{}{ + "name": "Africa", + "countries": []interface{}{}, + }, + }, + } + tmpPath := filepath.Join(tmpDir, "empty_countries.json") + data, _ := json.Marshal(invalidConfig) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "continents.0.countries: Array must have at least 1 items") { + t.Errorf("Expected error containing 'continents.0.countries: Array must have at least 1 items', got: %v", err) + } + }) + + t.Run("InvalidCountryCode", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + countries := continents[0].(map[string]interface{})["countries"].([]interface{}) + country := countries[0].(map[string]interface{}) + country["code"] = "D1" + tmpPath := filepath.Join(tmpDir, "invalid_country_code.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "code: Does not match pattern '^[A-Z]{2}$'") { + t.Errorf("Expected error containing 'code: Does not match pattern '^[A-Z]{2}$'', got: %v", err) + } + }) + + t.Run("DuplicateCountryCode", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + countries := continents[0].(map[string]interface{})["countries"].([]interface{}) + duplicateCountry := countries[0] + countries = append(countries, duplicateCountry) + continents[0].(map[string]interface{})["countries"] = countries + tmpPath := filepath.Join(tmpDir, "duplicate_country_code.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err != nil { + t.Errorf("Expected no error since schema allows duplicate country codes, got: %v", err) + } + }) + + t.Run("EmptyIDTypes", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + countries := continents[0].(map[string]interface{})["countries"].([]interface{}) + country := countries[0].(map[string]interface{}) + country["id_types"] = []interface{}{} + tmpPath := filepath.Join(tmpDir, "empty_id_types.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "continents.0.countries.0.id_types: Array must have at least 1 items") { + t.Errorf("Expected error containing 'continents.0.countries.0.id_types: Array must have at least 1 items', got: %v", err) + } + }) + + t.Run("EmptyIDType", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + countries := continents[0].(map[string]interface{})["countries"].([]interface{}) + idTypes := countries[0].(map[string]interface{})["id_types"].([]interface{}) + idType := idTypes[0].(map[string]interface{}) + idType["type"] = "" + tmpPath := filepath.Join(tmpDir, "empty_id_type.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "type: String length must be greater than or equal to 1") { + t.Errorf("Expected error containing 'type: String length must be greater than or equal to 1', got: %v", err) + } + }) + + t.Run("InvalidVerificationMethod", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + countries := continents[0].(map[string]interface{})["countries"].([]interface{}) + idTypes := countries[0].(map[string]interface{})["id_types"].([]interface{}) + idType := idTypes[0].(map[string]interface{}) + idType["verification_method"] = "invalid" + tmpPath := filepath.Join(tmpDir, "invalid_verification_method.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "continents.0.countries.0.id_types.0.verification_method: continents.0.countries.0.id_types.0.verification_method must be one of the following: \"biometric_kyc\", \"doc_verification\"") { + t.Errorf("Expected error containing 'continents.0.countries.0.id_types.0.verification_method must be one of the following: \"biometric_kyc\", \"doc_verification\"', got: %v", err) + } + }) + + t.Run("AddNewCountry", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + for i, continent := range continents { + if continent.(map[string]interface{})["name"] == "Africa" { + countries := continent.(map[string]interface{})["countries"].([]interface{}) + newCountry := map[string]interface{}{ + "name": "Testlandia", + "code": "XX", + "id_types": []interface{}{ + map[string]interface{}{ + "type": "PASSPORT", + "verification_method": "doc_verification", + }, + }, + } + countries = append(countries, newCountry) + continent.(map[string]interface{})["countries"] = countries + continents[i] = continent + break + } + } + config["continents"] = continents + tmpPath := filepath.Join(tmpDir, "add_new_country.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err != nil { + t.Errorf("Expected no error when adding new country, got: %v", err) + } + }) + + t.Run("AdditionalProperties", func(t *testing.T) { + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read config: %v", err) + } + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse config: %v", err) + } + + continents := config["continents"].([]interface{}) + countries := continents[0].(map[string]interface{})["countries"].([]interface{}) + country := countries[0].(map[string]interface{}) + country["extra_field"] = "invalid" + tmpPath := filepath.Join(tmpDir, "additional_properties.json") + data, _ = json.Marshal(config) + os.WriteFile(tmpPath, data, 0644) + + err = ValidateSmileIDConfig(tmpPath) + if err == nil { + t.Errorf("Expected schema validation error, got nil") + } else if !strings.Contains(err.Error(), "continents.0.countries.0: Additional property extra_field is not allowed") { + t.Errorf("Expected error containing 'continents.0.countries.0: Additional property extra_field is not allowed', got: %v", err) + } + }) +} + +// TestFullConfig tests the full id_types.json +func TestFullConfig(t *testing.T) { + // Validate the config + err := ValidateSmileIDConfig(configPath) + if err != nil { + t.Fatalf("Validation failed for full config: %v", err) + } + + // Load config for additional checks + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("Failed to read full config: %v", err) + } + var config SmileIDConfig + if err := json.Unmarshal(data, &config); err != nil { + t.Fatalf("Failed to parse full config: %v", err) + } + + // Verify region coverage (6 regions) + expectedRegions := map[string]bool{ + "Africa": false, + "Asia and the Middle East": false, + "Europe": false, + "North America": false, + "Oceania": false, + "South America": false, + } + for _, continent := range config.Continents { + if _, exists := expectedRegions[continent.Name]; exists { + expectedRegions[continent.Name] = true + } + } + for region, found := range expectedRegions { + if !found { + t.Errorf("Region %s not found in config", region) + } + } + + // Verify country count (>= 50) + totalCountries := 0 + for _, continent := range config.Continents { + totalCountries += len(continent.Countries) + } + if totalCountries < 50 { + t.Errorf("Expected at least 50 countries, got %d", totalCountries) + } + + // Verify ID type count (>= 200) + totalIDTypes := 0 + for _, continent := range config.Continents { + for _, country := range continent.Countries { + totalIDTypes += len(country.IDTypes) + } + } + if totalIDTypes < 200 { + t.Errorf("Expected at least 200 ID types, got %d", totalIDTypes) + } + + // Spot-check specific countries + for _, continent := range config.Continents { + for _, country := range continent.Countries { + if continent.Name == "Africa" && country.Code == "DZ" { + if len(country.IDTypes) != 6 { + t.Errorf("Expected 6 ID types for Algeria, got %d", len(country.IDTypes)) + } + foundDriversLicense := false + for _, idType := range country.IDTypes { + if idType.Type == "DRIVERS_LICENSE" { + foundDriversLicense = true + break + } + } + if !foundDriversLicense { + t.Errorf("Expected DRIVERS_LICENSE for Algeria") + } + } + if continent.Name == "Africa" && country.Code == "AO" { + if len(country.IDTypes) != 9 { + t.Errorf("Expected 9 ID types for Angola, got %d", len(country.IDTypes)) + } + foundVoterID := false + for _, idType := range country.IDTypes { + if idType.Type == "VOTER_ID" { + foundVoterID = true + break + } + } + if !foundVoterID { + t.Errorf("Expected VOTER_ID for Angola") + } + } + } + } +} diff --git a/services/kyc/smile/index.go b/services/kyc/smile/index.go new file mode 100644 index 00000000..6d02b3a1 --- /dev/null +++ b/services/kyc/smile/index.go @@ -0,0 +1,336 @@ +package smile + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "slices" + "time" + + fastshot "github.com/opus-domini/fast-shot" + "github.com/paycrest/aggregator/config" + "github.com/paycrest/aggregator/ent" + "github.com/paycrest/aggregator/ent/identityverificationrequest" + kycErrors "github.com/paycrest/aggregator/services/kyc/errors" + "github.com/paycrest/aggregator/storage" + "github.com/paycrest/aggregator/types" + "github.com/paycrest/aggregator/utils" +) + +//go:embed id_types.json +var idTypesJSON []byte + +type SmileIDService struct { + identityConf *config.IdentityConfiguration + serverConf *config.ServerConfiguration + db *ent.Client +} + +func NewSmileIDService() types.KYCProvider { + return &SmileIDService{ + identityConf: config.IdentityConfig(), + serverConf: config.ServerConfig(), + db: storage.Client, + } +} + +// RequestVerification implements the KYCProvider interface +func (s *SmileIDService) RequestVerification(ctx context.Context, req types.VerificationRequest) (*types.VerificationResponse, error) { + ivr, err := s.db.IdentityVerificationRequest. + Query(). + Where(identityverificationrequest.WalletAddressEQ(req.WalletAddress)). + Only(ctx) + if err != nil { + if !ent.IsNotFound(err) { + return nil, kycErrors.ErrDatabase{Err: err} + } + } + + timestamp := time.Now() + + if ivr != nil { + if ivr.WalletSignature == req.Signature { + return nil, kycErrors.ErrSignatureAlreadyUsed{} + } + + expiryPeriod := 15 * time.Minute + + if ivr.Status == identityverificationrequest.StatusFailed || (ivr.Status == identityverificationrequest.StatusPending && ivr.LastURLCreatedAt.Add(expiryPeriod).Before(timestamp)) { + _, err := s.db.IdentityVerificationRequest. + Delete(). + Where(identityverificationrequest.WalletAddressEQ(req.WalletAddress)). + Exec(ctx) + if err != nil { + return nil, kycErrors.ErrDatabase{Err: err} + } + } else if ivr.Status == identityverificationrequest.StatusPending && (ivr.LastURLCreatedAt.Add(expiryPeriod).Equal(timestamp) || ivr.LastURLCreatedAt.Add(expiryPeriod).After(timestamp)) { + _, err = ivr. + Update(). + SetWalletSignature(req.Signature). + Save(ctx) + if err != nil { + return nil, kycErrors.ErrDatabase{Err: err} + } + return &types.VerificationResponse{ + URL: ivr.VerificationURL, + ExpiresAt: ivr.LastURLCreatedAt, + }, nil + } + + if ivr.Status == identityverificationrequest.StatusSuccess { + return nil, kycErrors.ErrAlreadyVerified{} + } + } + + // Load and flatten the JSON file + idTypes, err := loadSmileIDConfig(idTypesJSON) + if err != nil { + return nil, fmt.Errorf("failed to load ID types: %v", err) + } + + smileIDSignature := s.getSmileIDSignature(timestamp.Format(time.RFC3339Nano)) + res, err := fastshot.NewClient(s.identityConf.SmileIdentityBaseUrl). + Config().SetTimeout(30 * time.Second). + Build().POST("/v1/smile_links"). + Body().AsJSON(map[string]interface{}{ + "partner_id": s.identityConf.SmileIdentityPartnerId, + "signature": smileIDSignature, + "timestamp": timestamp, + "name": "Aggregator KYC", + "company_name": "Paycrest", + "id_types": idTypes, + "callback_url": fmt.Sprintf("%s/v1/kyc/webhook", s.serverConf.ServerURL), + "data_privacy_policy_url": "https://paycrest.notion.site/KYC-Policy-10e2482d45a280e191b8d47d76a8d242", + "logo_url": "https://res.cloudinary.com/de6e0wihu/image/upload/v1738088043/xxhlrsld2wy9lzekahur.png", + "is_single_use": true, + "user_id": req.WalletAddress, + "expires_at": timestamp.Add(1 * time.Hour).Format(time.RFC3339Nano), + }). + Send() + if err != nil { + return nil, kycErrors.ErrProviderUnreachable{Err: err} + } + + data, err := utils.ParseJSONResponse(res.RawResponse) + if err != nil { + return nil, kycErrors.ErrProviderResponse{Err: fmt.Errorf("%v, %v", err, data)} + } + + ivr, err = s.db.IdentityVerificationRequest. + Create(). + SetWalletAddress(req.WalletAddress). + SetWalletSignature(req.Signature). + SetPlatform("smile_id"). + SetPlatformRef(data["ref_id"].(string)). + SetVerificationURL(data["link"].(string)). + SetLastURLCreatedAt(timestamp). + Save(ctx) + if err != nil { + return nil, kycErrors.ErrDatabase{Err: err} + } + + return &types.VerificationResponse{ + URL: ivr.VerificationURL, + ExpiresAt: ivr.LastURLCreatedAt, + }, nil +} + +// CheckStatus implements the KYCProvider interface +func (s *SmileIDService) CheckStatus(ctx context.Context, walletAddress string) (*types.VerificationStatus, error) { + ivr, err := s.db.IdentityVerificationRequest. + Query(). + Where(identityverificationrequest.WalletAddressEQ(walletAddress)). + Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, fmt.Errorf("no verification request found for this wallet address") + } + return nil, kycErrors.ErrDatabase{Err: err} + } + + response := &types.VerificationStatus{ + URL: ivr.VerificationURL, + Status: ivr.Status.String(), + } + + // Check if the verification URL has expired + if ivr.LastURLCreatedAt.Add(1*time.Hour).Before(time.Now()) && ivr.Status == identityverificationrequest.StatusPending { + response.Status = "expired" + } + + return response, nil +} + +// HandleWebhook implements the KYCProvider interface +func (s *SmileIDService) HandleWebhook(ctx context.Context, payload []byte) error { + var smilePayload SmileIDWebhookPayload + + // Parse the JSON payload + if err := json.Unmarshal(payload, &smilePayload); err != nil { + return fmt.Errorf("invalid payload") + } + + if !s.verifySmileIDWebhookSignature(smilePayload, smilePayload.Signature) { + return fmt.Errorf("invalid signature") + } + + // Process the webhook + status := identityverificationrequest.StatusPending + + // Check for success codes + successCodes := []string{ + "0810", // Document Verified + "1020", // Exact Match (Basic KYC and Enhanced KYC) + "1012", // Valid ID / ID Number Validated (Enhanced KYC) + "0820", // Authenticate User Machine Judgement - PASS + "0840", // Enroll User PASS - Machine Judgement + } + + // Check for failed codes + failedCodes := []string{ + "0811", // No Face Match + "0812", // Filed Security Features Check + "0813", // Document Not Verified - Machine Judgement + "1022", // No Match + "1023", // No Found + "1011", // Invalid ID / ID Number Invalid + "1013", // ID Number Not Found + "1014", // Unsupported ID Type + "0821", // Images did not match + "0911", // No Face Found + "0912", // Face Not Matching + "0921", // Face Not Found + "0922", // Selfie Quality Too Poor + "0841", // Enroll User FAIL + "0941", // Face Not Found + "0942", // Face Poor Quality + } + + if slices.Contains(successCodes, smilePayload.ResultCode) { + status = identityverificationrequest.StatusSuccess + } + if slices.Contains(failedCodes, smilePayload.ResultCode) { + status = identityverificationrequest.StatusFailed + } + + // Update the verification status in the database + _, err := s.db.IdentityVerificationRequest. + Update(). + Where( + identityverificationrequest.WalletAddressEQ(smilePayload.PartnerParams.UserID), + identityverificationrequest.StatusEQ(identityverificationrequest.StatusPending), + ). + SetStatus(status). + Save(ctx) + if err != nil { + return kycErrors.ErrDatabase{Err: err} + } + + return nil +} + +// verifyWebhookSignature verifies the signature of a Smile Identity webhook +func (s *SmileIDService) verifySmileIDWebhookSignature(payload SmileIDWebhookPayload, receivedSignature string) bool { + computedSignature := s.getSmileIDSignature(payload.Timestamp) + return computedSignature == receivedSignature +} + +// getSmileIDSignature generates a signature for a Smile ID request +func (s *SmileIDService) getSmileIDSignature(timestamp string) string { + h := hmac.New(sha256.New, []byte(s.identityConf.SmileIdentityApiKey)) + h.Write([]byte(timestamp)) + h.Write([]byte(s.identityConf.SmileIdentityPartnerId)) + h.Write([]byte("sid_request")) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +// FlattenSmileIDConfig converts the hierarchical SmileIDConfig into a flat array of id_types +func flattenSmileIDConfig(config SmileIDConfig) ([]map[string]interface{}, error) { + var idTypes []map[string]interface{} + + for _, continent := range config.Continents { + for _, country := range continent.Countries { + for _, idType := range country.IDTypes { + idTypeEntry := map[string]interface{}{ + "country": country.Code, + "id_type": idType.Type, + "verification_method": idType.VerificationMethod, + } + idTypes = append(idTypes, idTypeEntry) + } + } + } + + if len(idTypes) == 0 { + return nil, fmt.Errorf("no ID types found in configuration") + } + + return idTypes, nil +} + +// LoadSmileIDConfig loads and flattens the JSON data +func loadSmileIDConfig(data []byte) ([]map[string]interface{}, error) { + // Parse the JSON into SmileIDConfig + var config SmileIDConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + // Flatten the structure + return flattenSmileIDConfig(config) +} + +// getModuleRootDir finds the root directory of the Go module +func getModuleRootDir() (string, error) { + // Get the path to the current file + _, filename, _, _ := runtime.Caller(0) + dir := filepath.Dir(filename) + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("could not find module root (no go.mod file found)") + } + dir = parent + } +} + +type IDType struct { + Type string `json:"type"` + VerificationMethod string `json:"verification_method"` +} + +type Country struct { + Name string `json:"name"` + Code string `json:"code"` + IDTypes []IDType `json:"id_types"` +} + +type Continent struct { + Name string `json:"name"` + Countries []Country `json:"countries"` +} + +type SmileIDConfig struct { + Continents []Continent `json:"continents"` +} + +// SmileIDWebhookPayload represents the payload structure from Smile Identity +type SmileIDWebhookPayload struct { + ResultCode string `json:"ResultCode"` + PartnerParams struct { + UserID string `json:"user_id"` + } `json:"PartnerParams"` + Signature string `json:"signature"` + Timestamp string `json:"timestamp"` + // Add other fields as needed +} diff --git a/services/kyc/smile/index_test.go b/services/kyc/smile/index_test.go new file mode 100644 index 00000000..b3845da2 --- /dev/null +++ b/services/kyc/smile/index_test.go @@ -0,0 +1,456 @@ +package smile + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "crypto/hmac" + "crypto/sha256" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/jarcoal/httpmock" + _ "github.com/mattn/go-sqlite3" + "github.com/paycrest/aggregator/config" + "github.com/paycrest/aggregator/ent/enttest" + "github.com/paycrest/aggregator/ent/identityverificationrequest" + kycErrors "github.com/paycrest/aggregator/services/kyc/errors" + db "github.com/paycrest/aggregator/storage" + "github.com/paycrest/aggregator/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func generateValidSignature(t *testing.T, walletAddress, nonce string) string { + // This is a test private key - never use in production + privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19") + require.NoError(t, err) + + message := fmt.Sprintf("I accept the KYC Policy and hereby request an identity verification check for %s with nonce %s", walletAddress, nonce) + prefix := "\x19Ethereum Signed Message:\n" + fmt.Sprint(len(message)) + hash := crypto.Keccak256Hash([]byte(prefix + message)) + + signature, err := crypto.Sign(hash.Bytes(), privateKey) + require.NoError(t, err) + + // Add recovery ID + signature[64] += 27 + + return hex.EncodeToString(signature) +} + +func TestSmileIDService(t *testing.T) { + // Set up test database client + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&_fk=1") + defer client.Close() + + db.Client = client + + // Activate httpmock + httpmock.Activate() + defer httpmock.Deactivate() + + // Mock the Smile ID API response + httpmock.RegisterResponder("POST", "https://testapi.smileidentity.com/v1/smile_links", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "link": "https://links.usesmileid.com/1234/abcd", + "ref_id": "abcd1234", + }) + }, + ) + + service := &SmileIDService{ + identityConf: &config.IdentityConfiguration{ + SmileIdentityBaseUrl: "https://testapi.smileidentity.com", + SmileIdentityPartnerId: "1234", + SmileIdentityApiKey: "test_api_key", + }, + serverConf: &config.ServerConfiguration{ + ServerURL: "https://api.example.com", + }, + db: client, + } + + // ==================== RequestVerification Tests ==================== + t.Run("RequestVerification", func(t *testing.T) { + // Clear any existing verification requests before each test + _, err := client.IdentityVerificationRequest.Delete().Exec(context.Background()) + require.NoError(t, err) + + // Test: Valid request creates new verification + t.Run("Valid request creates new verification", func(t *testing.T) { + // Generate a valid wallet address and signature + walletAddress := "0x96216849c49358B10257cb55b28eA603c874b05E" + nonce := "test_nonce_123" + signature := generateValidSignature(t, walletAddress, nonce) + + // Create request payload + payload := types.VerificationRequest{ + WalletAddress: walletAddress, + Signature: signature, + Nonce: nonce, + } + + // Call the method + resp, err := service.RequestVerification(context.Background(), payload) + + // Assertions + require.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "https://links.usesmileid.com/1234/abcd", resp.URL) + + // Verify database entry + ivr, err := client.IdentityVerificationRequest. + Query(). + Where(identityverificationrequest.WalletAddressEQ(walletAddress)). + Only(context.Background()) + require.NoError(t, err) + assert.Equal(t, walletAddress, ivr.WalletAddress) + assert.Equal(t, signature, ivr.WalletSignature) + assert.Equal(t, "smile_id", ivr.Platform.String()) + assert.Equal(t, "abcd1234", ivr.PlatformRef) + assert.Equal(t, "https://links.usesmileid.com/1234/abcd", ivr.VerificationURL) + assert.Equal(t, identityverificationrequest.StatusPending, ivr.Status) + }) + + // Test: Already verified wallet + t.Run("Already verified wallet", func(t *testing.T) { + // Clear any existing verification requests + _, err := client.IdentityVerificationRequest.Delete().Exec(context.Background()) + require.NoError(t, err) + + // Create a verified entry in the database + walletAddress := "0x96216849c49358B10257cb55b28eA603c874b05E" + nonce := "test_nonce_456" + signature := generateValidSignature(t, walletAddress, nonce) + + _, err = client.IdentityVerificationRequest. + Create(). + SetWalletAddress(walletAddress). + SetWalletSignature("previous_signature"). + SetPlatform("smile_id"). + SetPlatformRef("ref123"). + SetVerificationURL("https://example.com"). + SetStatus(identityverificationrequest.StatusSuccess). + SetLastURLCreatedAt(time.Now()). + Save(context.Background()) + require.NoError(t, err) + + // Try to request verification again + payload := types.VerificationRequest{ + WalletAddress: walletAddress, + Signature: signature, + Nonce: nonce, + } + + resp, err := service.RequestVerification(context.Background(), payload) + + assert.Error(t, err) + assert.Nil(t, resp) + assert.IsType(t, kycErrors.ErrAlreadyVerified{}, err) + }) + + // Test: Reuse same signature + t.Run("Reuse same signature", func(t *testing.T) { + // Clear any existing verification requests + _, err := client.IdentityVerificationRequest.Delete().Exec(context.Background()) + require.NoError(t, err) + + // Create an entry with a specific signature + walletAddress := "0x96216849c49358B10257cb55b28eA603c874b05E" + nonce := "test_nonce_789" + signature := generateValidSignature(t, walletAddress, nonce) + + _, err = client.IdentityVerificationRequest. + Create(). + SetWalletAddress(walletAddress). + SetWalletSignature(signature). + SetPlatform("smile_id"). + SetPlatformRef("ref456"). + SetVerificationURL("https://example.com"). + SetStatus(identityverificationrequest.StatusPending). + SetLastURLCreatedAt(time.Now()). + Save(context.Background()) + require.NoError(t, err) + + // Try to request verification with the same signature + payload := types.VerificationRequest{ + WalletAddress: walletAddress, + Signature: signature, + Nonce: nonce, + } + + resp, err := service.RequestVerification(context.Background(), payload) + + assert.Error(t, err) + assert.Nil(t, resp) + assert.IsType(t, kycErrors.ErrSignatureAlreadyUsed{}, err) + }) + + t.Run("Pending but not expired", func(t *testing.T) { + // Clear any existing verification requests + _, err := client.IdentityVerificationRequest.Delete().Exec(context.Background()) + require.NoError(t, err) + + // Create a pending entry that's not expired + walletAddress := "0x96216849c49358B10257cb55b28eA603c874b05E" + oldSignature := "old_signature" + nonce := "test_nonce_101112" + newSignature := generateValidSignature(t, walletAddress, nonce) + verificationURL := "https://links.usesmileid.com/pending/notexpired" + + lastURLCreatedAt := time.Now().Add(-5 * time.Minute) // 5 minutes ago, not expired + + _, err = client.IdentityVerificationRequest. + Create(). + SetWalletAddress(walletAddress). + SetWalletSignature(oldSignature). + SetPlatform("smile_id"). + SetPlatformRef("ref789"). + SetVerificationURL(verificationURL). + SetStatus(identityverificationrequest.StatusPending). + SetLastURLCreatedAt(lastURLCreatedAt). + Save(context.Background()) + require.NoError(t, err) + + // Request with new signature + payload := types.VerificationRequest{ + WalletAddress: walletAddress, + Signature: newSignature, + Nonce: nonce, + } + + resp, err := service.RequestVerification(context.Background(), payload) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, verificationURL, resp.URL) + }) + }) + + // ==================== CheckStatus Tests ==================== + t.Run("CheckStatus", func(t *testing.T) { + // Create test data + pendingWallet := "0x1234567890123456789012345678901234567890" + successWallet := "0x2345678901234567890123456789012345678901" + failedWallet := "0x3456789012345678901234567890123456789012" + expiredWallet := "0x4567890123456789012345678901234567890123" + nonExistentWallet := "0x5678901234567890123456789012345678901234" + + // Create pending verification + _, err := client.IdentityVerificationRequest. + Create(). + SetWalletAddress(pendingWallet). + SetWalletSignature("sig1"). + SetPlatform("smile_id"). + SetPlatformRef("ref1"). + SetVerificationURL("https://example.com/pending"). + SetStatus(identityverificationrequest.StatusPending). + SetLastURLCreatedAt(time.Now()). + Save(context.Background()) + require.NoError(t, err) + + // Create successful verification + _, err = client.IdentityVerificationRequest. + Create(). + SetWalletAddress(successWallet). + SetWalletSignature("sig2"). + SetPlatform("smile_id"). + SetPlatformRef("ref2"). + SetVerificationURL("https://example.com/success"). + SetStatus(identityverificationrequest.StatusSuccess). + SetLastURLCreatedAt(time.Now()). + Save(context.Background()) + require.NoError(t, err) + + // Create failed verification + _, err = client.IdentityVerificationRequest. + Create(). + SetWalletAddress(failedWallet). + SetWalletSignature("sig3"). + SetPlatform("smile_id"). + SetPlatformRef("ref3"). + SetVerificationURL("https://example.com/failed"). + SetStatus(identityverificationrequest.StatusFailed). + SetLastURLCreatedAt(time.Now()). + Save(context.Background()) + require.NoError(t, err) + + // Create expired verification + _, err = client.IdentityVerificationRequest. + Create(). + SetWalletAddress(expiredWallet). + SetWalletSignature("sig4"). + SetPlatform("smile_id"). + SetPlatformRef("ref4"). + SetVerificationURL("https://example.com/expired"). + SetStatus(identityverificationrequest.StatusPending). + SetLastURLCreatedAt(time.Now().Add(-2 * time.Hour)). // 2 hours ago, expired + Save(context.Background()) + require.NoError(t, err) + + // Test: Pending verification + t.Run("Pending verification", func(t *testing.T) { + resp, err := service.CheckStatus(context.Background(), pendingWallet) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "pending", resp.Status) + assert.Equal(t, "https://example.com/pending", resp.URL) + }) + + // Test: Successful verification + t.Run("Successful verification", func(t *testing.T) { + resp, err := service.CheckStatus(context.Background(), successWallet) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "success", resp.Status) + assert.Equal(t, "https://example.com/success", resp.URL) + }) + + // Test: Failed verification + t.Run("Failed verification", func(t *testing.T) { + resp, err := service.CheckStatus(context.Background(), failedWallet) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "failed", resp.Status) + assert.Equal(t, "https://example.com/failed", resp.URL) + }) + + // Test: Expired verification + t.Run("Expired verification", func(t *testing.T) { + resp, err := service.CheckStatus(context.Background(), expiredWallet) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "expired", resp.Status) + assert.Equal(t, "https://example.com/expired", resp.URL) + }) + + // Test: Non-existent wallet + t.Run("Non-existent wallet", func(t *testing.T) { + resp, err := service.CheckStatus(context.Background(), nonExistentWallet) + + assert.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "no verification request found for this wallet address") + }) + }) + + // ==================== HandleWebhook Tests ==================== + t.Run("HandleWebhook", func(t *testing.T) { + // Create test data - a pending verification + _, err := client.IdentityVerificationRequest.Delete().Exec(context.Background()) + require.NoError(t, err) + + // Create test data - a pending verification + testWallet := "0x1234567890123456789012345678901234567890" + _, err = client.IdentityVerificationRequest. + Create(). + SetWalletAddress(testWallet). + SetWalletSignature("sig1"). + SetPlatform("smile_id"). + SetPlatformRef("ref1"). + SetVerificationURL("https://example.com/pending"). + SetStatus(identityverificationrequest.StatusPending). + SetLastURLCreatedAt(time.Now()). + Save(context.Background()) + require.NoError(t, err) + + // Helper function to create a valid webhook payload + createWebhookPayload := func(resultCode string, userID string) []byte { + timestamp := time.Now().Format(time.RFC3339Nano) + + // Generate a valid signature + h := hmac.New(sha256.New, []byte("test_api_key")) + h.Write([]byte(timestamp)) + h.Write([]byte("1234")) + h.Write([]byte("sid_request")) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + payload := SmileIDWebhookPayload{ + ResultCode: resultCode, + PartnerParams: struct { + UserID string `json:"user_id"` + }{ + UserID: userID, + }, + Signature: signature, + Timestamp: timestamp, + } + + jsonData, _ := json.Marshal(payload) + return jsonData + } + + // Test: Success webhook + t.Run("Success webhook", func(t *testing.T) { + // Create a webhook payload with a success code + payloadBytes := createWebhookPayload("0810", testWallet) + + err := service.HandleWebhook(context.Background(), payloadBytes) + assert.NoError(t, err) + + // Verify the status was updated + ivr, err := client.IdentityVerificationRequest. + Query(). + Where(identityverificationrequest.WalletAddressEQ(testWallet)). + Only(context.Background()) + require.NoError(t, err) + assert.Equal(t, identityverificationrequest.StatusSuccess, ivr.Status) + }) + + // Test: Failed webhook + t.Run("Failed webhook", func(t *testing.T) { + // Reset the status to pending + _, err := client.IdentityVerificationRequest. + Update(). + Where(identityverificationrequest.WalletAddressEQ(testWallet)). + SetStatus(identityverificationrequest.StatusPending). + Save(context.Background()) + require.NoError(t, err) + + // Create a webhook payload with a failure code + payloadBytes := createWebhookPayload("0811", testWallet) + + err = service.HandleWebhook(context.Background(), payloadBytes) + assert.NoError(t, err) + + // Verify the status was updated + ivr, err := client.IdentityVerificationRequest. + Query(). + Where(identityverificationrequest.WalletAddressEQ(testWallet)). + Only(context.Background()) + require.NoError(t, err) + assert.Equal(t, identityverificationrequest.StatusFailed, ivr.Status) + }) + + // Test: Invalid signature + t.Run("Invalid signature", func(t *testing.T) { + // Create an invalid webhook payload + payload := SmileIDWebhookPayload{ + ResultCode: "0810", + PartnerParams: struct { + UserID string `json:"user_id"` + }{ + UserID: testWallet, + }, + Signature: "invalid_signature", + Timestamp: time.Now().Format(time.RFC3339Nano), + } + jsonData, _ := json.Marshal(payload) + + err := service.HandleWebhook(context.Background(), jsonData) + assert.Error(t, err) + assert.Equal(t, "invalid signature", err.Error()) + }) + }) +} diff --git a/services/kyc/types.go b/services/kyc/types.go deleted file mode 100644 index 08597ddf..00000000 --- a/services/kyc/types.go +++ /dev/null @@ -1,37 +0,0 @@ -package kyc - -import ( - "time" -) - - -// NewIDVerificationRequest is the request for a new identity verification request -type NewIDVerificationRequest struct { - WalletAddress string `json:"walletAddress" binding:"required"` - Signature string `json:"signature" binding:"required"` - Nonce string `json:"nonce" binding:"required"` -} - -// NewIDVerificationResponse is the response for a new identity verification request -type NewIDVerificationResponse struct { - URL string `json:"url"` - ExpiresAt time.Time `json:"expiresAt"` -} - -type IDVerificationStatusResponse struct { - Status string `json:"status"` - URL string `json:"url"` -} - -// SmileIDWebhookPayload represents the payload structure from Smile Identity -type SmileIDWebhookPayload struct { - ResultCode string `json:"ResultCode"` - PartnerParams struct { - UserID string `json:"user_id"` - } `json:"PartnerParams"` - Signature string `json:"signature"` - Timestamp string `json:"timestamp"` - // Add other fields as needed -} - - diff --git a/services/order/evm.go b/services/order/evm.go index e9813219..c9dc43f9 100644 --- a/services/order/evm.go +++ b/services/order/evm.go @@ -2,8 +2,6 @@ package order import ( "context" - "crypto/rand" - "encoding/base64" "encoding/hex" "fmt" "math/big" @@ -504,7 +502,7 @@ func (s *OrderEVM) transferCallData(recipient common.Address, amount *big.Int) ( // createOrderCallData creates the data for the createOrder method func (s *OrderEVM) createOrderCallData(order *ent.PaymentOrder) ([]byte, error) { // Encrypt recipient details - encryptedOrderRecipient, err := s.encryptOrderRecipient(order.Edges.Recipient) + encryptedOrderRecipient, err := cryptoUtils.EncryptOrderRecipient(order.Edges.Recipient) if err != nil { return nil, fmt.Errorf("failed to encrypt recipient details: %w", err) } @@ -747,7 +745,7 @@ func (s *OrderEVM) settleCallData(ctx context.Context, order *ent.LockPaymentOrd return nil, fmt.Errorf("failed to parse GatewayOrder ABI: %w", err) } - institution, err := utils.GetInstitutionByCode(ctx, order.Institution) + institution, err := utils.GetInstitutionByCode(ctx, order.Institution, true) if err != nil { return nil, fmt.Errorf("failed to get institution: %w", err) } @@ -766,6 +764,7 @@ func (s *OrderEVM) settleCallData(ctx context.Context, order *ent.LockPaymentOrd providerordertoken.HasCurrencyWith( fiatcurrency.CodeEQ(institution.Edges.FiatCurrency.Code), ), + providerordertoken.AddressNEQ(""), ). Only(ctx) if err != nil { @@ -797,30 +796,3 @@ func (s *OrderEVM) settleCallData(ctx context.Context, order *ent.LockPaymentOrd return data, nil } - -// encryptOrderRecipient encrypts the recipient details -func (s *OrderEVM) encryptOrderRecipient(recipient *ent.PaymentOrderRecipient) (string, error) { - // Generate a cryptographically secure random nonce - nonce := make([]byte, 32) - if _, err := rand.Read(nonce); err != nil { - return "", fmt.Errorf("failed to generate nonce: %w", err) - } - message := struct { - Nonce string - AccountIdentifier string - AccountName string - Institution string - ProviderID string - Memo string - }{ - base64.StdEncoding.EncodeToString(nonce), recipient.AccountIdentifier, recipient.AccountName, recipient.Institution, recipient.ProviderID, recipient.Memo, - } - - // Encrypt with the public key of the aggregator - messageCipher, err := cryptoUtils.PublicKeyEncryptJSON(message, cryptoConf.AggregatorPublicKey) - if err != nil { - return "", fmt.Errorf("failed to encrypt message: %w", err) - } - - return base64.StdEncoding.EncodeToString(messageCipher), nil -} diff --git a/services/order/tron.go b/services/order/tron.go index 484429ac..d5ce79a2 100644 --- a/services/order/tron.go +++ b/services/order/tron.go @@ -3,9 +3,7 @@ package order import ( "context" "crypto/ecdsa" - "crypto/rand" "crypto/sha256" - "encoding/base64" "encoding/hex" "errors" "fmt" @@ -28,6 +26,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/protobuf/proto" + "github.com/paycrest/aggregator/ent/fiatcurrency" "github.com/paycrest/aggregator/ent/lockorderfulfillment" "github.com/paycrest/aggregator/ent/lockpaymentorder" networkent "github.com/paycrest/aggregator/ent/network" @@ -418,7 +417,7 @@ func (s *OrderTron) approveCallData(spender util.Address, amount *big.Int) ([]by // createOrderCallData creates the data for the createOrder method func (s *OrderTron) createOrderCallData(order *ent.PaymentOrder) ([]byte, error) { // Encrypt recipient details - encryptedOrderRecipient, err := s.encryptOrderRecipient(order.Edges.Recipient) + encryptedOrderRecipient, err := cryptoUtils.EncryptOrderRecipient(order.Edges.Recipient) if err != nil { return nil, fmt.Errorf("failed to encrypt recipient details: %w", err) } @@ -557,6 +556,11 @@ func (s *OrderTron) settleCallData(ctx context.Context, order *ent.LockPaymentOr return nil, fmt.Errorf("failed to parse GatewayOrder ABI: %w", err) } + institution, err := utils.GetInstitutionByCode(ctx, order.Institution, true) + if err != nil { + return nil, fmt.Errorf("failed to get institution: %w", err) + } + // Fetch provider address from db token, err := db.Client.ProviderOrderToken. Query(). @@ -568,6 +572,10 @@ func (s *OrderTron) settleCallData(ctx context.Context, order *ent.LockPaymentOr providerordertoken.HasTokenWith( tokenent.IDEQ(order.Edges.Token.ID), ), + providerordertoken.HasCurrencyWith( + fiatcurrency.CodeEQ(institution.Edges.FiatCurrency.Code), + ), + providerordertoken.AddressNEQ(""), ). Only(ctx) if err != nil { @@ -608,33 +616,6 @@ func (s *OrderTron) settleCallData(ctx context.Context, order *ent.LockPaymentOr return data, nil } -// encryptOrderRecipient encrypts the recipient details -func (s *OrderTron) encryptOrderRecipient(recipient *ent.PaymentOrderRecipient) (string, error) { - // Generate a cryptographically secure random nonce - nonce := make([]byte, 32) - if _, err := rand.Read(nonce); err != nil { - return "", fmt.Errorf("failed to generate nonce: %w", err) - } - message := struct { - Nonce string - AccountIdentifier string - AccountName string - Institution string - ProviderID string - Memo string - }{ - base64.StdEncoding.EncodeToString(nonce), recipient.AccountIdentifier, recipient.AccountName, recipient.Institution, recipient.ProviderID, recipient.Memo, - } - - // Encrypt with the public key of the aggregator - messageCipher, err := cryptoUtils.PublicKeyEncryptJSON(message, cryptoConf.AggregatorPublicKey) - if err != nil { - return "", fmt.Errorf("failed to encrypt message: %w", err) - } - - return base64.StdEncoding.EncodeToString(messageCipher), nil -} - // signTransaction signs a transaction with a private key func (s *OrderTron) signTransaction(transaction *api.TransactionExtention, privateKey *ecdsa.PrivateKey) (*api.TransactionExtention, error) { rawData, err := proto.Marshal(transaction.Transaction.GetRawData()) diff --git a/services/priority_queue.go b/services/priority_queue.go index 5c92e108..59c504b4 100644 --- a/services/priority_queue.go +++ b/services/priority_queue.go @@ -80,8 +80,7 @@ func (s *PriorityQueueService) GetProvisionBuckets(ctx context.Context) ([]*ent. // GetProviderRate returns the rate for a provider func (s *PriorityQueueService) GetProviderRate(ctx context.Context, provider *ent.ProviderProfile, tokenSymbol string, currency string) (decimal.Decimal, error) { // Fetch the token config for the provider - tokenConfig, err := storage.Client.ProviderOrderToken. - Query(). + tokenConfig, err := provider.QueryOrderTokens(). Where( providerordertoken.HasProviderWith(providerprofile.IDEQ(provider.ID)), providerordertoken.HasTokenWith(token.SymbolEQ(tokenSymbol)), @@ -141,13 +140,19 @@ func (s *PriorityQueueService) CreatePriorityQueueForBucket(ctx context.Context, // Delete the previous queue err := s.deleteQueue(ctx, prevRedisKey) if err != nil && err != context.Canceled { - logger.Errorf("failed to delete previous provider queue: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Key": prevRedisKey, + }).Errorf("failed to delete previous provider queue") } // Copy the current queue to the previous queue prevData, err := storage.RedisClient.LRange(ctx, redisKey, 0, -1).Result() if err != nil && err != context.Canceled { - logger.Errorf("failed to fetch provider rates: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Key": redisKey, + }).Errorf("failed to fetch provider rates") } // Convert []string to []interface{} @@ -160,14 +165,21 @@ func (s *PriorityQueueService) CreatePriorityQueueForBucket(ctx context.Context, if len(prevValues) > 0 { err = storage.RedisClient.RPush(ctx, prevRedisKey, prevValues...).Err() if err != nil && err != context.Canceled { - logger.Errorf("failed to store previous provider rates: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Key": prevRedisKey, + "Values": prevValues, + }).Errorf("failed to store previous provider rates") } } // Delete the current queue err = s.deleteQueue(ctx, redisKey) if err != nil && err != context.Canceled { - logger.Errorf("failed to delete existing circular queue: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Key": redisKey, + }).Errorf("failed to delete existing circular queue") } // TODO: add also the checks for all the currencies that a provider has @@ -189,7 +201,11 @@ func (s *PriorityQueueService) CreatePriorityQueueForBucket(ctx context.Context, All(ctx) if err != nil { if err != context.Canceled { - logger.Errorf("failed to get tokens for provider %s: %v", provider.ID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + "Currency": bucket.Edges.Currency.Code, + }).Errorf("failed to get tokens for provider") } continue } @@ -204,7 +220,12 @@ func (s *PriorityQueueService) CreatePriorityQueueForBucket(ctx context.Context, rate, err := s.GetProviderRate(ctx, provider, orderToken.Edges.Token.Symbol, bucket.Edges.Currency.Code) if err != nil { if err != context.Canceled { - logger.Errorf("failed to get %s rate for provider %s: %v", orderToken.Edges.Token.Symbol, provider.ID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": provider.ID, + "Token": orderToken.Edges.Token.Symbol, + "Currency": bucket.Edges.Currency.Code, + }).Errorf("failed to get rate for provider") } continue } @@ -229,7 +250,11 @@ func (s *PriorityQueueService) CreatePriorityQueueForBucket(ctx context.Context, // Enqueue the serialized data into the circular queue err = storage.RedisClient.RPush(ctx, redisKey, data).Err() if err != nil && err != context.Canceled { - logger.Errorf("failed to enqueue provider data to circular queue: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Key": redisKey, + "Data": data, + }).Errorf("failed to enqueue provider data to circular queue") } } } @@ -241,7 +266,11 @@ func (s *PriorityQueueService) AssignLockPaymentOrder(ctx context.Context, order excludeList, err := storage.RedisClient.LRange(ctx, fmt.Sprintf("order_exclude_list_%s", order.ID), 0, -1).Result() if err != nil { - logger.Errorf("%s - failed to get exclude list: %v", order.ID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + }).Errorf("failed to get exclude list") return err } @@ -261,7 +290,11 @@ func (s *PriorityQueueService) AssignLockPaymentOrder(ctx context.Context, order if order.UpdatedAt.Before(time.Now().Add(-10 * time.Minute)) { order.Rate, err = s.GetProviderRate(ctx, provider, order.Token.Symbol, order.ProvisionBucket.Edges.Currency.Code) if err != nil { - logger.Errorf("%s - failed to get rate for provider %s: %v", orderIDPrefix, order.ProviderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + }).Errorf("failed to get rate for provider") } _, err = storage.Client.PaymentOrder. Update(). @@ -269,16 +302,28 @@ func (s *PriorityQueueService) AssignLockPaymentOrder(ctx context.Context, order SetRate(order.Rate). Save(ctx) if err != nil { - logger.Errorf("%s - failed to update rate for provider %s: %v", orderIDPrefix, order.ProviderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + }).Errorf("failed to update rate for provider") } } err = s.sendOrderRequest(ctx, order) if err == nil { return nil } - logger.Errorf("%s - failed to send order request to specific provider %s: %v", orderIDPrefix, order.ProviderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + }).Errorf("failed to send order request to specific provider") } else { - logger.Errorf("%s - failed to get provider: %v", orderIDPrefix, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + }).Errorf("failed to get provider") } if provider.VisibilityMode == providerprofile.VisibilityModePrivate { @@ -295,7 +340,7 @@ func (s *PriorityQueueService) AssignLockPaymentOrder(ctx context.Context, order if err != nil { prevRedisKey := redisKey + "_prev" err = s.matchRate(ctx, prevRedisKey, orderIDPrefix, order, excludeList) - if err != nil && !strings.Contains(err.Error(), "redis: nil") { + if err != nil && !strings.Contains(fmt.Sprintf("%v", err), "redis: nil") { return err } } @@ -316,21 +361,34 @@ func (s *PriorityQueueService) sendOrderRequest(ctx context.Context, order types } if err := storage.RedisClient.HSet(ctx, orderKey, orderRequestData).Err(); err != nil { - logger.Errorf("failed to map order to a provider in Redis: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + "orderKey": orderKey, + }).Errorf("failed to map order to a provider in Redis") return err } // Set a TTL for the order request err := storage.RedisClient.ExpireAt(ctx, orderKey, time.Now().Add(orderConf.OrderRequestValidity)).Err() if err != nil { - logger.Errorf("failed to set TTL for order request: %v", err) + // logger.Errorf("failed to set TTL for order request: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "orderKey": orderKey, + }).Errorf("failed to set TTL for order request") return err } // Notify the provider orderRequestData["orderId"] = order.ID if err := s.notifyProvider(ctx, orderRequestData); err != nil { - logger.Errorf("failed to notify provider %s: %v", order.ProviderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + }).Errorf("failed to notify provider") return err } @@ -378,7 +436,11 @@ func (s *PriorityQueueService) notifyProvider(ctx context.Context, orderRequestD data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("PriorityQueueService.notifyProvider: %v %v", err, data) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": providerID, + }).Errorf("failed to parse JSON response after new order request with data: %v", data) + return err } return nil @@ -409,7 +471,12 @@ func (s *PriorityQueueService) matchRate(ctx context.Context, redisKey string, o // Extract the rate from the data (assuming it's in the format "providerID:token:rate:minAmount:maxAmount") parts := strings.Split(providerData, ":") if len(parts) != 5 { - logger.Errorf("%s - invalid data format at index %d: %s", orderIDPrefix, index, providerData) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + "ProviderData": providerData, + }).Errorf("invalid data format at index %d when matching rate", index) continue // Skip this entry due to invalid format } @@ -437,8 +504,15 @@ func (s *PriorityQueueService) matchRate(ctx context.Context, redisKey string, o } normalizedAmount := order.Amount - if strings.EqualFold(order.Token.BaseCurrency, order.ProvisionBucket.Edges.Currency.Code) && order.Token.BaseCurrency != "USD" { - rateResponse, err := utils.GetTokenRateFromQueue("USDT", normalizedAmount, order.ProvisionBucket.Edges.Currency.Code, order.ProvisionBucket.Edges.Currency.MarketRate) + bucketCurrency := order.ProvisionBucket.Edges.Currency + if bucketCurrency == nil { + bucketCurrency, err = order.ProvisionBucket.QueryCurrency().Only(ctx) + if err != nil { + continue + } + } + if strings.EqualFold(order.Token.BaseCurrency, bucketCurrency.Code) && order.Token.BaseCurrency != "USD" { + rateResponse, err := utils.GetTokenRateFromQueue("USDT", normalizedAmount, bucketCurrency.Code, bucketCurrency.MarketRate) if err != nil { continue } @@ -454,21 +528,62 @@ func (s *PriorityQueueService) matchRate(ctx context.Context, redisKey string, o continue } - // TODO: make the slippage of 0.5 configurable by provider - if rate.Sub(order.Rate).Abs().LessThanOrEqual(decimal.NewFromFloat(0.5)) { + network := order.Token.Edges.Network + if network == nil { + network, err = order.Token.QueryNetwork().Only(ctx) + if err != nil { + continue + } + } + + providerToken, err := storage.Client.ProviderOrderToken. + Query(). + Where( + providerordertoken.NetworkEQ(network.Identifier), + providerordertoken.HasProviderWith( + providerprofile.IDEQ(order.ProviderID), + providerprofile.IsAvailableEQ(true), + ), + providerordertoken.HasTokenWith(token.IDEQ(order.Token.ID)), + providerordertoken.HasCurrencyWith( + fiatcurrency.CodeEQ(bucketCurrency.Code), + ), + providerordertoken.AddressNEQ(""), + ). + First(ctx) + if err != nil { + continue + } + + // Calculate allowed deviation based on slippage + allowedDeviation := order.Rate.Mul(providerToken.RateSlippage.Div(decimal.NewFromInt(100))) + + if rate.Sub(order.Rate).Abs().LessThanOrEqual(allowedDeviation) { // Found a match for the rate if index == 0 { // Match found at index 0, perform LPOP to dequeue data, err := storage.RedisClient.LPop(ctx, redisKey).Result() if err != nil { - logger.Errorf("%s - failed to dequeue from circular queue: %v", orderIDPrefix, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + "redisKey": redisKey, + "orderIDPrefix": orderIDPrefix, + }).Errorf("failed to dequeue from circular queue when matching rate") return err } // Enqueue data to the end of the queue err = storage.RedisClient.RPush(ctx, redisKey, data).Err() if err != nil { - logger.Errorf("%s - failed to enqueue to circular queue: %v", orderIDPrefix, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + "redisKey": redisKey, + "orderIDPrefix": orderIDPrefix, + }).Errorf("failed to enqueue to circular queue when matching rate") return err } } @@ -476,13 +591,25 @@ func (s *PriorityQueueService) matchRate(ctx context.Context, redisKey string, o // Assign the order to the provider and save it to Redis err = s.sendOrderRequest(ctx, order) if err != nil { - logger.Errorf("%s - failed to send order request to specific provider %s: %v", orderIDPrefix, order.ProviderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + "redisKey": redisKey, + "orderIDPrefix": orderIDPrefix, + }).Errorf("failed to send order request to specific provider when matching rate") // Push provider ID to order exclude list orderKey := fmt.Sprintf("order_exclude_list_%s", order.ID) _, err = storage.RedisClient.RPush(ctx, orderKey, order.ProviderID).Result() if err != nil { - logger.Errorf("%s - error pushing provider %s to order_exclude_list on Redis: %v", orderIDPrefix, order.ProviderID, err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.ProviderID, + "redisKey": redisKey, + "orderIDPrefix": orderIDPrefix, + }).Errorf("failed to push provider to order exclude list when matching rate") } // Reassign the lock payment order to another provider diff --git a/services/priority_queue_test.go b/services/priority_queue_test.go index 6604452d..4dd3aede 100644 --- a/services/priority_queue_test.go +++ b/services/priority_queue_test.go @@ -104,7 +104,7 @@ func setupForPQ() error { "provider": publicProviderProfile, "currency_id": currency.ID, "network": token.Edges.Network.Identifier, - "tokenID": token.ID, + "token_id": token.ID, }, ) if err != nil { @@ -154,7 +154,7 @@ func setupForPQ() error { // Set up payment order _, err = test.CreateTestLockPaymentOrder(map[string]interface{}{ "provider": privateProviderProfile, - "tokenID": testCtxForPQ.token.ID, + "token_id": testCtxForPQ.token.ID, "gateway_id": "order-12345", }) if err != nil { @@ -162,7 +162,7 @@ func setupForPQ() error { } _, err = test.CreateTestLockPaymentOrder(map[string]interface{}{ "provider": publicProviderProfile, - "tokenID": testCtxForPQ.token.ID, + "token_id": testCtxForPQ.token.ID, }) if err != nil { return err @@ -263,7 +263,7 @@ func TestPriorityQueueTest(t *testing.T) { _order, err := test.CreateTestLockPaymentOrder(map[string]interface{}{ "provider": testCtxForPQ.publicProviderProfile, "rate": 100.0, - "tokenID": testCtxForPQ.token.ID, + "token_id": testCtxForPQ.token.ID, "gateway_id": "order-1", }) assert.NoError(t, err) @@ -317,7 +317,7 @@ func TestPriorityQueueTest(t *testing.T) { assert.NoError(t, err) _order, err := test.CreateTestLockPaymentOrder(map[string]interface{}{ "provider": testCtxForPQ.publicProviderProfile, - "tokenID": testCtxForPQ.token.ID, + "token_id": testCtxForPQ.token.ID, "gateway_id": "order-1234", }) assert.NoError(t, err) @@ -339,6 +339,18 @@ func TestPriorityQueueTest(t *testing.T) { assert.NoError(t, err) + // Setup httpmock for sendOrderRequest + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", testCtxForPQ.publicProviderProfile.HostIdentifier+"/new_order", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(200, map[string]interface{}{ + "status": "success", + "message": "Order processed successfully", + }) + }) + err = service.sendOrderRequest(context.Background(), types.LockPaymentOrderFields{ ID: order.ID, ProviderID: testCtxForPQ.publicProviderProfile.ID, @@ -406,7 +418,7 @@ func TestPriorityQueueTest(t *testing.T) { // assert.NoError(t, err) // _order, err := test.CreateTestLockPaymentOrder(map[string]interface{}{ // "provider": testCtxForPQ.publicProviderProfile, - // "tokenID": testCtxForPQ.token.ID, + // "token_id": testCtxForPQ.token.ID, // "status": lockpaymentorder.StatusProcessing.String(), // }) // assert.NoError(t, err) @@ -435,7 +447,7 @@ func TestPriorityQueueTest(t *testing.T) { // assert.NoError(t, err) // _order, err := test.CreateTestLockPaymentOrder(map[string]interface{}{ // "provider": testCtxForPQ.privateProviderProfile, - // "tokenID": testCtxForPQ.token.ID, + // "token_id": testCtxForPQ.token.ID, // "status": lockpaymentorder.StatusProcessing.String(), // "updatedAt": time.Now().Add(-5 * time.Minute), // }) diff --git a/services/slack_test.go b/services/slack_test.go index 0c793e9e..d72d7c9b 100644 --- a/services/slack_test.go +++ b/services/slack_test.go @@ -10,6 +10,7 @@ import ( "github.com/jarcoal/httpmock" "github.com/paycrest/aggregator/config" "github.com/paycrest/aggregator/ent" + "github.com/paycrest/aggregator/utils" "github.com/stretchr/testify/assert" ) @@ -98,5 +99,27 @@ func TestSlackService(t *testing.T) { assert.NoError(t, err, "unexpected error") }) + + t.Run("FormatTimestampToGMT1 should work with any timezone configuration", func(t *testing.T) { + testTime := time.Date(2023, 5, 15, 14, 30, 0, 0, time.UTC) + + formattedTime, err := utils.FormatTimestampToGMT1(testTime) + + assert.NoError(t, err, "formatting timestamp should not produce an error") + assert.NotEmpty(t, formattedTime, "formatted time should not be empty") + assert.Contains(t, formattedTime, "May 15, 2023") + assert.Contains(t, formattedTime, "3:30 PM") + }) + + t.Run("FormatTimestampToGMT1 should work with current time", func(t *testing.T) { + // Get current time + now := time.Now().UTC() + + formattedTime, err := utils.FormatTimestampToGMT1(now) + + assert.NoError(t, err, "formatting current timestamp should not produce an error") + assert.NotEmpty(t, formattedTime, "formatted time should not be empty") + assert.Contains(t, formattedTime, now.In(time.FixedZone("GMT+1", 3600)).Format("2006")) + }) }) } diff --git a/tasks/tasks.go b/tasks/tasks.go index 03eed566..1944e5a5 100644 --- a/tasks/tasks.go +++ b/tasks/tasks.go @@ -69,7 +69,10 @@ func setRPCClients(ctx context.Context) ([]*ent.Network, error) { return err }) if retryErr != nil { - logger.Errorf("failed to connect to %s RPC %v", network.Identifier, retryErr) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Network": network.Identifier, + }).Errorf("failed to connect to RPC client") continue } @@ -135,7 +138,15 @@ func RetryStaleUserOperations() error { } err := service.CreateOrder(ctx, rpcClients[order.Edges.Token.Edges.Network.Identifier], order.ID) if err != nil { - logger.Errorf("RetryStaleUserOperations.CreateOrder %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "AmountPaid": order.AmountPaid, + "Amount": order.Amount, + "PercentSettled": order.PercentSettled, + "GatewayID": order.GatewayID, + "NetworkIdentifier": order.Edges.Token.Edges.Network.Identifier, + }).Errorf("RetryStaleUserOperations.CreateOrder") } } } @@ -171,7 +182,13 @@ func RetryStaleUserOperations() error { } err := service.SettleOrder(ctx, rpcClients[order.Edges.Token.Edges.Network.Identifier], order.ID) if err != nil { - logger.Errorf("RetryStaleUserOperations.SettleOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "Amount": order.Amount, + "GatewayID": order.GatewayID, + "NetworkIdentifier": order.Edges.Token.Edges.Network.Identifier, + }).Errorf("RetryStaleUserOperations.SettleOrder") } } }(ctx) @@ -230,7 +247,13 @@ func RetryStaleUserOperations() error { } err := service.RefundOrder(ctx, rpcClients[order.Edges.Token.Edges.Network.Identifier], order.Edges.Token.Edges.Network, order.GatewayID) if err != nil { - logger.Errorf("RetryStaleUserOperations.RefundOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "Amount": order.Amount, + "GatewayID": order.GatewayID, + "NetworkIdentifier": order.Edges.Token.Edges.Network.Identifier, + }).Errorf("RetryStaleUserOperations.RefundOrder") } } }(ctx) @@ -257,7 +280,13 @@ func RetryStaleUserOperations() error { service := orderService.NewOrderEVM() err = service.CreateOrder(ctx, rpcClients[order.Edges.Token.Edges.Network.Identifier], order.ID) if err != nil { - logger.Errorf("RetryStaleUserOperations.RetryLinkedAddress: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "Amount": order.Amount, + "GatewayID": order.GatewayID, + "NetworkIdentifier": order.Edges.Token.Edges.Network.Identifier, + }).Errorf("RetryStaleUserOperations.RetryLinkedAddress") } } }(ctx) @@ -301,7 +330,10 @@ func IndexBlockchainEvents() error { WithRecipient(). All(ctx) if err != nil && err != context.Canceled { - logger.Errorf("IndexBlockchainEvents: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Status": paymentorder.StatusInitiated, + }).Errorf("Failed to index blockchain events for unused receive addresses") } for _, order := range orders { @@ -408,7 +440,13 @@ func IndexBlockchainEvents() error { Order(ent.Asc(lockpaymentorder.FieldBlockNumber)). All(ctx) if err != nil && err != context.Canceled { - logger.Errorf("IndexBlockchainEvents: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Status": lockpaymentorder.StatusValidated, + "Network": network.Identifier, + "NetworkID": network.ID, + "FieldGatewayID": lockpaymentorder.FieldGatewayID, + }).Errorf("Failed to index blockchain events for unused receive addresses") } for _, order := range lockOrders { @@ -462,7 +500,13 @@ func IndexBlockchainEvents() error { Order(ent.Asc(lockpaymentorder.FieldBlockNumber)). All(ctx) if err != nil && err != context.Canceled { - logger.Errorf("IndexBlockchainEvents: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Status": lockpaymentorder.StatusPending, + "Network": network.Identifier, + "NetworkID": network.ID, + "FieldGatewayID": lockpaymentorder.FieldGatewayID, + }).Errorf("Failed to index blockchain events for unused receive addresses") } if len(lockOrders) > 0 { @@ -509,7 +553,12 @@ func IndexLinkedAddresses() error { WithNetwork(). All(ctx) if err != nil && err != context.Canceled { - logger.Errorf("IndexLinkedAddresses: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Status": tokenent.IsEnabled(true), + "Reason": "db:No tokens found", + }).Errorf("Failed to index blockchain events for linked addresses") + return } for _, token := range tokens { @@ -540,7 +589,11 @@ func ReassignPendingOrders() { ClearProvider(). Save(ctx) if err != nil { - logger.Errorf("ReassignPendingOrders.db: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Status": lockpaymentorder.StatusPending, + "Reason": "db: No pending orders found", + }).Errorf("Failed to reassign pending orders") return } @@ -560,7 +613,11 @@ func ReassignPendingOrders() { ). All(ctx) if err != nil { - logger.Errorf("ReassignPendingOrders.db: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "Status": lockpaymentorder.StatusPending, + "Reason": "db: No pending orders found", + }).Errorf("Failed to reassign pending orders") return } @@ -569,7 +626,12 @@ func ReassignPendingOrders() { orderKey := fmt.Sprintf("order_request_%s", order.ID) exists, err := storage.RedisClient.Exists(ctx, orderKey).Result() if err != nil { - logger.Errorf("ReassignPendingOrders.redis: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "OrderKey": orderKey, + "Reason": "redis: No pending orders found", + }).Errorf("Redis: Failed to reassign pending orders") continue } @@ -595,12 +657,73 @@ func ReassignPendingOrders() { err := services.NewPriorityQueueService().AssignLockPaymentOrder(ctx, lockPaymentOrder) if err != nil { - logger.Errorf("failed to reassign declined order request: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "OrderKey": orderKey, + "GatewayID": order.GatewayID, + }).Errorf("Redis: Failed to reassign declined order request") } } } } +// reassignCancelledOrder reassigns cancelled orders to providers +func reassignCancelledOrder(ctx context.Context, order *ent.LockPaymentOrder, fulfillment *ent.LockOrderFulfillment) { + if order.Edges.Provider.VisibilityMode != providerprofile.VisibilityModePrivate && order.CancellationCount < orderConf.RefundCancellationCount { + // Push provider ID to order exclude list + orderKey := fmt.Sprintf("order_exclude_list_%s", order.ID) + _, err := storage.RedisClient.RPush(ctx, orderKey, order.Edges.Provider.ID).Result() + if err != nil { + return + } + + _, err = storage.Client.LockPaymentOrder. + UpdateOneID(order.ID). + ClearProvider(). + SetStatus(lockpaymentorder.StatusPending). + Save(ctx) + if err != nil { + return + } + + if fulfillment != nil { + err = storage.Client.LockOrderFulfillment. + DeleteOneID(fulfillment.ID). + Exec(ctx) + if err != nil { + return + } + } + + // Reassign the order to a provider + lockPaymentOrder := types.LockPaymentOrderFields{ + ID: order.ID, + Token: order.Edges.Token, + GatewayID: order.GatewayID, + Amount: order.Amount, + Rate: order.Rate, + BlockNumber: order.BlockNumber, + Institution: order.Institution, + AccountIdentifier: order.AccountIdentifier, + AccountName: order.AccountName, + ProviderID: "", + Memo: order.Memo, + ProvisionBucket: order.Edges.ProvisionBucket, + } + + err = services.NewPriorityQueueService().AssignLockPaymentOrder(ctx, lockPaymentOrder) + if err != nil { + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "OrderKey": orderKey, + "GatewayID": order.GatewayID, + }).Errorf("Redis: Failed to reassign declined order request") + } + } +} + // SyncLockOrderFulfillments syncs lock order fulfillments func SyncLockOrderFulfillments() { // ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -666,15 +789,30 @@ func SyncLockOrderFulfillments() { continue } if len(order.Edges.Fulfillments) == 0 { + if order.Status == lockpaymentorder.StatusCancelled { + reassignCancelledOrder(ctx, order, nil) + continue + } + // Compute HMAC decodedSecret, err := base64.StdEncoding.DecodeString(order.Edges.Provider.Edges.APIKey.Secret) if err != nil { - logger.Errorf("SyncLockOrderFulfillments.DecodeSecret: %v %v", err, order.Edges.Provider.ID) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.Edges.Provider.ID, + "Reason": "internal: Failed to decode provider secret", + }).Errorf("SyncLockOrderFulfillments.DecodeSecret") continue } decryptedSecret, err := cryptoUtils.DecryptPlain(decodedSecret) if err != nil { - logger.Errorf("SyncLockOrderFulfillments.DecryptSecret: %v %v", err, order.Edges.Provider.ID) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.Edges.Provider.ID, + "Reason": "internal: Failed to decrypt provider secret", + }).Errorf("SyncLockOrderFulfillments.DecryptSecret") continue } @@ -692,16 +830,27 @@ func SyncLockOrderFulfillments() { Body().AsJSON(payload). Send() if err != nil { - logger.Errorf("SyncLockOrderFulfillments: %v %v", err, payload) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": order.Edges.Provider.ID, + "PayloadOrderId": payload["orderId"], + "PayloadCurrency": payload["currency"], + "Reason": "internal: Failed to send tx_status request to provider", + }).Errorf("SyncLockOrderFulfillments.SendTxStatusRequest") // Set status to pending on 400 error - if strings.Contains(err.Error(), "400") { + if strings.Contains(fmt.Sprintf("%v", err), "400") { _, updateErr := storage.Client.LockPaymentOrder. UpdateOneID(order.ID). SetStatus(lockpaymentorder.StatusPending). Save(ctx) if updateErr != nil { - logger.Errorf("SyncLockOrderFulfillments.UpdateStatus: %v", updateErr) + logger.WithFields(logger.Fields{ + "Error": updateErr, + "OrderID": order.ID.String(), + "ProviderID": order.Edges.Provider.ID, + "Reason": "internal: Failed to update order status", + }).Errorf("SyncLockOrderFulfillments.UpdateStatus") } } continue @@ -710,13 +859,22 @@ func SyncLockOrderFulfillments() { data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { if order.Status == lockpaymentorder.StatusProcessing && order.UpdatedAt.Add(orderConf.OrderFulfillmentValidity*2).Before(time.Now()) { - logger.Errorf("SyncLockOrderFulfillments.StuckProcessing: %v %v", err, payload) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderID": order.Edges.Provider.ID, + "PayloadOrderId": payload["orderId"], + "PayloadCurrency": payload["currency"], + }).Errorf("Failed to parse JSON response after getting trx status from provider") // delete lock order to trigger re-indexing err := storage.Client.LockPaymentOrder. DeleteOneID(order.ID). Exec(ctx) if err != nil { - logger.Errorf("SyncLockOrderFulfillments.DeleteOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.Edges.Provider.ID, + }).Errorf("Failed to delete order after failing to parse JSON response when getting trx status from provider") } continue } @@ -787,12 +945,20 @@ func SyncLockOrderFulfillments() { // Compute HMAC decodedSecret, err := base64.StdEncoding.DecodeString(order.Edges.Provider.Edges.APIKey.Secret) if err != nil { - logger.Errorf("SyncLockOrderFulfillments.DecodeSecret: %v %v", err, order.Edges.Provider.ID) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.Edges.Provider.ID, + }).Errorf("Failed to decode provider secret for pending fulfillment") continue } decryptedSecret, err := cryptoUtils.DecryptPlain(decodedSecret) if err != nil { - logger.Errorf("SyncLockOrderFulfillments.DecryptSecret: %v %v", err, order.Edges.Provider.ID) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.Edges.Provider.ID, + }).Errorf("Failed to decrypt provider secret for pending fulfillment") continue } @@ -818,7 +984,15 @@ func SyncLockOrderFulfillments() { data, err := utils.ParseJSONResponse(res.RawResponse) if err != nil { - logger.Errorf("SyncLockOrderFulfillments: %v %v", err, payload) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "ProviderID": order.Edges.Provider.ID, + "PayloadOrderId": payload["orderId"], + "PayloadCurrency": payload["currency"], + "PayloadPsp": payload["psp"], + "PayloadTxId": payload["txId"], + }).Errorf("Failed to parse JSON response after getting trx status from provider") continue } @@ -876,51 +1050,9 @@ func SyncLockOrderFulfillments() { } } else if fulfillment.ValidationStatus == lockorderfulfillment.ValidationStatusFailed { - if order.Edges.Provider.VisibilityMode != providerprofile.VisibilityModePrivate { - // Push provider ID to order exclude list - orderKey := fmt.Sprintf("order_exclude_list_%s", order.ID) - _, err = storage.RedisClient.RPush(ctx, orderKey, order.Edges.Provider.ID).Result() - if err != nil { - continue - } - - _, err = storage.Client.LockPaymentOrder. - UpdateOneID(order.ID). - ClearProvider(). - SetStatus(lockpaymentorder.StatusPending). - Save(ctx) - if err != nil { - continue - } - - err = storage.Client.LockOrderFulfillment. - DeleteOneID(fulfillment.ID). - Exec(ctx) - if err != nil { - continue - } - - // Reassign the order to a provider - lockPaymentOrder := types.LockPaymentOrderFields{ - ID: order.ID, - Token: order.Edges.Token, - GatewayID: order.GatewayID, - Amount: order.Amount, - Rate: order.Rate, - BlockNumber: order.BlockNumber, - Institution: order.Institution, - AccountIdentifier: order.AccountIdentifier, - AccountName: order.AccountName, - ProviderID: "", - Memo: order.Memo, - ProvisionBucket: order.Edges.ProvisionBucket, - } + reassignCancelledOrder(ctx, order, fulfillment) + continue - err = services.NewPriorityQueueService().AssignLockPaymentOrder(ctx, lockPaymentOrder) - if err != nil { - logger.Errorf("SyncLockOrderFulfillments.AssignLockPaymentOrder: %v", err) - } - } } else if fulfillment.ValidationStatus == lockorderfulfillment.ValidationStatusSuccess { transactionLog, err := storage.Client.TransactionLog. Create(). @@ -957,7 +1089,10 @@ func ReassignStaleOrderRequest(ctx context.Context, orderRequestChan <-chan *red orderUUID, err := uuid.Parse(orderID) if err != nil { - logger.Errorf("ReassignStaleOrderRequest: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": orderID, + }).Errorf("ReassignStaleOrderRequest: Failed to parse order ID") continue } @@ -970,7 +1105,11 @@ func ReassignStaleOrderRequest(ctx context.Context, orderRequestChan <-chan *red WithProvisionBucket(). Only(ctx) if err != nil { - logger.Errorf("ReassignStaleOrderRequest: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "UUID": orderUUID, + }).Errorf("ReassignStaleOrderRequest: Failed to get order from database") continue } @@ -990,7 +1129,13 @@ func ReassignStaleOrderRequest(ctx context.Context, orderRequestChan <-chan *red // Assign the order to a provider err = services.NewPriorityQueueService().AssignLockPaymentOrder(ctx, orderFields) if err != nil { - logger.Errorf("ReassignStaleOrderRequest.AssignLockPaymentOrder: %v", err) + // logger.Errorf("ReassignStaleOrderRequest.AssignLockPaymentOrder: %v", err) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "OrderID": order.ID.String(), + "UUID": orderUUID, + "GatewayID": order.GatewayID, + }).Errorf("ReassignStaleOrderRequest: Failed to assign order to provider") } } } @@ -1078,94 +1223,6 @@ func SubscribeToRedisKeyspaceEvents() { go ReassignStaleOrderRequest(ctx, orderRequestChan) } -// fetchExternalRate fetches the external rate for a fiat currency -func fetchExternalRate(currency string) (decimal.Decimal, error) { - currency = strings.ToUpper(currency) - supportedCurrencies := []string{"KES", "NGN", "GHS", "TZS", "UGX", "XOF"} - isSupported := false - for _, supported := range supportedCurrencies { - if currency == supported { - isSupported = true - break - } - } - if !isSupported { - return decimal.Zero, fmt.Errorf("ComputeMarketRate: currency not supported") - } - - // Fetch rates from third-party APIs - var price decimal.Decimal - if currency == "NGN" { - res, err := fastshot.NewClient("https://www.quidax.com"). - Config().SetTimeout(30*time.Second). - Build().GET(fmt.Sprintf("/api/v1/markets/tickers/usdt%s", strings.ToLower(currency))). - Retry().Set(3, 5*time.Second). - Send() - if err != nil { - return decimal.Zero, fmt.Errorf("ComputeMarketRate: %w", err) - } - - data, err := utils.ParseJSONResponse(res.RawResponse) - if err != nil { - return decimal.Zero, fmt.Errorf("ComputeMarketRate: %w %v", err, data) - } - - price, err = decimal.NewFromString(data["data"].(map[string]interface{})["ticker"].(map[string]interface{})["buy"].(string)) - if err != nil { - return decimal.Zero, fmt.Errorf("ComputeMarketRate: %w", err) - } - } else { - res, err := fastshot.NewClient("https://p2p.binance.com"). - Config().SetTimeout(30*time.Second). - Header().Add("Content-Type", "application/json"). - Build().POST("/bapi/c2c/v2/friendly/c2c/adv/search"). - Retry().Set(3, 5*time.Second). - Body().AsJSON(map[string]interface{}{ - "asset": "USDT", - "fiat": currency, - "tradeType": "SELL", - "page": 1, - "rows": 20, - }). - Send() - if err != nil { - return decimal.Zero, fmt.Errorf("ComputeMarketRate: %w", err) - } - - resData, err := utils.ParseJSONResponse(res.RawResponse) - if err != nil { - return decimal.Zero, fmt.Errorf("ComputeMarketRate: %w", err) - } - - // Access the data array - data, ok := resData["data"].([]interface{}) - if !ok || len(data) == 0 { - return decimal.Zero, fmt.Errorf("ComputeMarketRate: No data in the response") - } - - // Loop through the data array and extract prices - var prices []decimal.Decimal - for _, item := range data { - adv, ok := item.(map[string]interface{})["adv"].(map[string]interface{}) - if !ok { - continue - } - - price, err := decimal.NewFromString(adv["price"].(string)) - if err != nil { - continue - } - - prices = append(prices, price) - } - - // Calculate and return the median - price = utils.Median(prices) - } - - return price, nil -} - // ComputeMarketRate computes the market price for fiat currencies func ComputeMarketRate() error { // ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -1183,7 +1240,7 @@ func ComputeMarketRate() error { for _, currency := range currencies { // Fetch external rate - externalRate, err := fetchExternalRate(currency.Code) + externalRate, err := utils.FetchQuidaxRates(currency.Code) if err != nil { continue } @@ -1335,32 +1392,32 @@ func StartCronJobs() { err := ComputeMarketRate() if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for ComputeMarketRate: %v", err) } if serverConf.Environment != "production" { err = priorityQueue.ProcessBucketQueues() if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for ProcessBucketQueues: %v", err) } } // Compute market rate every 10 minutes _, err = scheduler.Every(10).Minutes().Do(ComputeMarketRate) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for ComputeMarketRate after 10 minutes: %v", err) } // Refresh provision bucket priority queues every X minutes _, err = scheduler.Every(orderConf.BucketQueueRebuildInterval).Minutes().Do(priorityQueue.ProcessBucketQueues) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for ProcessBucketQueues: %v", err) } // Retry failed webhook notifications every 59 minutes _, err = scheduler.Every(59).Minutes().Do(RetryFailedWebhookNotifications) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for RetryFailedWebhookNotifications: %v", err) } // Reassign pending order requests every 13 minutes @@ -1372,31 +1429,31 @@ func StartCronJobs() { // Sync lock order fulfillments every 1 minute _, err = scheduler.Every(1).Minute().Do(SyncLockOrderFulfillments) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for SyncLockOrderFulfillments: %v", err) } // Handle receive address validity every 31 minutes _, err = scheduler.Every(31).Minutes().Do(HandleReceiveAddressValidity) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for HandleReceiveAddressValidity: %v", err) } // Retry stale user operations every 2 minutes _, err = scheduler.Every(2).Minutes().Do(RetryStaleUserOperations) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for RetryStaleUserOperations: %v", err) } // Index blockchain events every 10 seconds _, err = scheduler.Every(10).Seconds().Do(IndexBlockchainEvents) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for IndexBlockchainEvents: %v", err) } // Index linked addresses every 1 minute _, err = scheduler.Every(1).Minute().Do(IndexLinkedAddresses) if err != nil { - logger.Errorf("StartCronJobs: %v", err) + logger.Errorf("StartCronJobs for IndexLinkedAddresses: %v", err) } // Start scheduler diff --git a/tasks/tasks_test.go b/tasks/tasks_test.go index bd4c3305..066ebb44 100644 --- a/tasks/tasks_test.go +++ b/tasks/tasks_test.go @@ -18,7 +18,6 @@ import ( "github.com/paycrest/aggregator/types" "github.com/paycrest/aggregator/utils" "github.com/paycrest/aggregator/utils/test" - "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" ) @@ -169,10 +168,4 @@ func TestTasks(t *testing.T) { assert.Equal(t, hook.Status, webhookretryattempt.StatusExpired) }) - - t.Run("fetchExternalRate", func(t *testing.T) { - value, err := fetchExternalRate("KSH") - assert.Error(t, err) - assert.Equal(t, value, decimal.Zero) - }) } diff --git a/types/types.go b/types/types.go index e97e2390..6898a0c2 100644 --- a/types/types.go +++ b/types/types.go @@ -101,6 +101,13 @@ type OrderService interface { SettleOrder(ctx context.Context, client RPCClient, orderID uuid.UUID) error } +// KYCProvider defines the interface for KYC verification providers +type KYCProvider interface { + RequestVerification(ctx context.Context, req VerificationRequest) (*VerificationResponse, error) + CheckStatus(ctx context.Context, walletAddress string) (*VerificationStatus, error) + HandleWebhook(ctx context.Context, payload []byte) error +} + // CreateOrderParams is the parameters for the create order payload type CreateOrderParams struct { Token common.Address @@ -112,6 +119,25 @@ type CreateOrderParams struct { MessageHash string } +// VerificationRequest represents a generic KYC verification request +type VerificationRequest struct { + WalletAddress string `json:"walletAddress"` + Signature string `json:"signature"` + Nonce string `json:"nonce"` +} + +// VerificationResponse represents a generic KYC verification response +type VerificationResponse struct { + URL string `json:"url"` + ExpiresAt time.Time `json:"expiresAt"` +} + +// VerificationStatus represents the status of a KYC verification +type VerificationStatus struct { + URL string `json:"url"` + Status string `json:"status"` +} + // RegisterPayload is the payload for the register endpoint type RegisterPayload struct { FirstName string `json:"firstName" binding:"required"` @@ -210,11 +236,12 @@ type SenderProfilePayload struct { type ProviderOrderTokenPayload struct { Currency string `json:"currency" binding:"required"` Symbol string `json:"symbol" binding:"required"` - ConversionRateType providerordertoken.ConversionRateType `json:"conversionRateType" binding:"required"` - FixedConversionRate decimal.Decimal `json:"fixedConversionRate" binding:"required"` + ConversionRateType providerordertoken.ConversionRateType `json:"conversionRateType" binding:"required,oneof=fixed floating"` + FixedConversionRate decimal.Decimal `json:"fixedConversionRate" binding:"required,gt=0"` FloatingConversionRate decimal.Decimal `json:"floatingConversionRate" binding:"required"` - MaxOrderAmount decimal.Decimal `json:"maxOrderAmount" binding:"required"` - MinOrderAmount decimal.Decimal `json:"minOrderAmount" binding:"required"` + MaxOrderAmount decimal.Decimal `json:"maxOrderAmount" binding:"required,gt=0"` + MinOrderAmount decimal.Decimal `json:"minOrderAmount" binding:"required,gt=0"` + RateSlippage decimal.Decimal `json:"rateSlippage" binding:"gte=0.1"` Address string `json:"address" binding:"required"` Network string `json:"network" binding:"required"` } @@ -317,6 +344,7 @@ type LockPaymentOrderFields struct { AccountName string ProviderID string Memo string + Metadata map[string]interface{} ProvisionBucket *ent.ProvisionBucket UpdatedAt time.Time CreatedAt time.Time @@ -381,13 +409,14 @@ type LockPaymentOrderStatusResponse struct { // PaymentOrderRecipient describes a payment order recipient type PaymentOrderRecipient struct { - Institution string `json:"institution" binding:"required"` - AccountIdentifier string `json:"accountIdentifier" binding:"required"` - AccountName string `json:"accountName" binding:"required"` - Memo string `json:"memo" binding:"required"` - ProviderID string `json:"providerId"` - Currency string `json:"currency"` - Nonce string `json:"nonce"` + Institution string `json:"institution" binding:"required"` + AccountIdentifier string `json:"accountIdentifier" binding:"required"` + AccountName string `json:"accountName" binding:"required"` + Memo string `json:"memo" binding:"required"` + ProviderID string `json:"providerId"` + Metadata map[string]interface{} `json:"metadata"` + Currency string `json:"currency"` + Nonce string `json:"nonce"` } // NewPaymentOrderPayload is the payload for the create payment order endpoint @@ -654,3 +683,25 @@ type SupportedTokenResponse struct { BaseCurrency string `json:"baseCurrency"` Network string `json:"network"` } + +// BitgetResponse is the response for Bitget advertisement endpoint +type BitgetResponse struct { + Code string `json:"code"` + Data BitgetData `json:"data"` + Msg string `json:"msg"` +} + +// BitgetData is the struct for Bitget advertisement data +type BitgetData struct { + DataList []BitgetAd `json:"dataList"` +} + +// BitgetAd is the struct for Bitget advertisement data item +type BitgetAd struct { + Price string `json:"price"` + CoinCode string `json:"coinCode"` + FiatCode string `json:"fiatCode"` + Amount string `json:"amount"` + MinAmount string `json:"minAmount"` + MaxAmount string `json:"maxAmount"` +} diff --git a/utils/crypto/crypto.go b/utils/crypto/crypto.go index 29d63699..fb980a88 100644 --- a/utils/crypto/crypto.go +++ b/utils/crypto/crypto.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/base64" "encoding/json" "encoding/pem" "fmt" @@ -16,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/common" hdwallet "github.com/miguelmota/go-ethereum-hdwallet" "github.com/paycrest/aggregator/config" + "github.com/paycrest/aggregator/ent" tronWallet "github.com/paycrest/tron-wallet" tronEnums "github.com/paycrest/tron-wallet/enums" "golang.org/x/crypto/bcrypt" @@ -246,3 +248,31 @@ func GenerateTronAccountFromIndex(accountIndex int) (wallet *tronWallet.TronWall return wallet, nil } + +// EncryptOrderRecipient encrypts the recipient details using the aggregator's public key +func EncryptOrderRecipient(recipient *ent.PaymentOrderRecipient) (string, error) { + // Generate a cryptographically secure random nonce + nonce := make([]byte, 32) + if _, err := rand.Read(nonce); err != nil { + return "", fmt.Errorf("failed to generate nonce: %w", err) + } + message := struct { + Nonce string + AccountIdentifier string + AccountName string + Institution string + ProviderID string + Memo string + Metadata map[string]interface{} + }{ + base64.StdEncoding.EncodeToString(nonce), recipient.AccountIdentifier, recipient.AccountName, recipient.Institution, recipient.ProviderID, recipient.Memo, recipient.Metadata, + } + + // Encrypt with the public key of the aggregator + messageCipher, err := PublicKeyEncryptJSON(message, cryptoConf.AggregatorPublicKey) + if err != nil { + return "", fmt.Errorf("failed to encrypt message: %w", err) + } + + return base64.StdEncoding.EncodeToString(messageCipher), nil +} diff --git a/utils/external_market.go b/utils/external_market.go new file mode 100644 index 00000000..172b9ddb --- /dev/null +++ b/utils/external_market.go @@ -0,0 +1,217 @@ +package utils + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + fastshot "github.com/opus-domini/fast-shot" + "github.com/paycrest/aggregator/types" + "github.com/shopspring/decimal" +) + +var ( + BitgetAPIURL = "https://www.bitget.com" + BinanceAPIURL = "https://p2p.binance.com" + QuidaxAPIURL = "https://www.quidax.com" +) + +// FetchExternalRate fetches the external rate for a fiat currency +func FetchExternalRate(currency string) (decimal.Decimal, error) { + currency = strings.ToUpper(currency) + supportedCurrencies := []string{"KES", "NGN", "GHS", "TZS", "UGX", "XOF"} + isSupported := false + for _, supported := range supportedCurrencies { + if currency == supported { + isSupported = true + break + } + } + if !isSupported { + return decimal.Zero, fmt.Errorf("ComputeMarketRate: currency not supported") + } + + var prices []decimal.Decimal + + // Fetch rates based on currency + if currency == "NGN" { + quidaxRate, err := FetchQuidaxRates(currency) + if err == nil { + prices = append(prices, quidaxRate) + } + } else { + binanceRates, err := FetchBinanceRates(currency) + if err == nil { + prices = append(prices, binanceRates...) + } + } + + // Fetch Bitget rates for all supported currencies + bitgetRates, err := FetchBitgetRates(currency) + if err == nil { + prices = append(prices, bitgetRates...) + } + + if len(prices) == 0 { + return decimal.Zero, fmt.Errorf("ComputeMarketRate: no valid rates found") + } + + // Return the median price + return Median(prices), nil +} + +// FetchQuidaxRate fetches the USDT exchange rate from Quidax (NGN only) +func FetchQuidaxRates(currency string) (decimal.Decimal, error) { + url := fmt.Sprintf("/api/v1/markets/tickers/usdt%s", strings.ToLower(currency)) + + res, err := fastshot.NewClient(QuidaxAPIURL). + Config().SetTimeout(30*time.Second). + Build().GET(url). + Retry().Set(3, 5*time.Second). + Send() + if err != nil { + return decimal.Zero, fmt.Errorf("FetchQuidaxRate: %w", err) + } + + data, err := ParseJSONResponse(res.RawResponse) + if err != nil { + return decimal.Zero, fmt.Errorf("FetchQuidaxRate: %w", err) + } + + price, err := decimal.NewFromString(data["data"].(map[string]interface{})["ticker"].(map[string]interface{})["buy"].(string)) + if err != nil { + return decimal.Zero, fmt.Errorf("FetchQuidaxRate: %w", err) + } + + return price, nil +} + +// FetchBinanceRates fetches USDT exchange rates from Binance P2P +func FetchBinanceRates(currency string) ([]decimal.Decimal, error) { + res, err := fastshot.NewClient(BinanceAPIURL). + Config().SetTimeout(30*time.Second). + Header().Add("Content-Type", "application/json"). + Build().POST("/bapi/c2c/v2/friendly/c2c/adv/search"). + Retry().Set(3, 5*time.Second). + Body().AsJSON(map[string]interface{}{ + "asset": "USDT", + "fiat": currency, + "tradeType": "SELL", + "page": 1, + "rows": 20, + }). + Send() + if err != nil { + return nil, fmt.Errorf("FetchBinanceRates: %w", err) + } + + resData, err := ParseJSONResponse(res.RawResponse) + if err != nil { + return nil, fmt.Errorf("FetchBinanceRates: %w", err) + } + + data, ok := resData["data"].([]interface{}) + if !ok || len(data) == 0 { + return nil, fmt.Errorf("FetchBinanceRates: no data in response") + } + + var prices []decimal.Decimal + for _, item := range data { + adv, ok := item.(map[string]interface{})["adv"].(map[string]interface{}) + if !ok { + continue + } + + price, err := decimal.NewFromString(adv["price"].(string)) + if err != nil { + continue + } + + prices = append(prices, price) + } + + if len(prices) == 0 { + return nil, fmt.Errorf("FetchBinanceRates: no valid prices found") + } + + return prices, nil +} + +// FetchBitgetRates fetches USDT exchange rates from Bitget P2P listings +func FetchBitgetRates(currency string) ([]decimal.Decimal, error) { + payload := map[string]interface{}{ + "side": 2, + "pageNo": 1, + "pageSize": 20, + "coinCode": "USDT", + "fiatCode": currency, + "languageType": 0, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("FetchBitgetRates: failed to marshal payload: %w", err) + } + + client := &http.Client{Timeout: 30 * time.Second} + req, err := http.NewRequest("POST", BitgetAPIURL+"/v1/p2p/pub/adv/queryAdvList", bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, fmt.Errorf("FetchBitgetRates: failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + var resp *http.Response + err = Retry(3, 5*time.Second, func() error { + var retryErr error + resp, retryErr = client.Do(req) + return retryErr + }) + if err != nil { + return nil, fmt.Errorf("FetchBitgetRates: failed to send request after retries: %w", err) + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("FetchBitgetRates: failed to read response body: %w", err) + } + + var resData types.BitgetResponse + err = json.Unmarshal(bodyBytes, &resData) + if err != nil { + fmt.Printf("FetchBitgetRates: failed to parse response, raw body: %s\n", string(bodyBytes)) + return nil, fmt.Errorf("FetchBitgetRates: failed to parse response: %w", err) + } + + if resData.Code != "00000" { + return nil, fmt.Errorf("FetchBitgetRates: API error: %s", resData.Msg) + } + + if len(resData.Data.DataList) == 0 { + return nil, fmt.Errorf("FetchBitgetRates: no sell ads found for %s/USDT", currency) + } + + var prices []decimal.Decimal + for i, ad := range resData.Data.DataList { + if ad.CoinCode != "USDT" || ad.FiatCode != currency { + continue + } + price, err := decimal.NewFromString(ad.Price) + if err != nil { + fmt.Printf("FetchBitgetRates: skipping ad at index %d with invalid price '%s': %v\n", i, ad.Price, err) + continue + } + prices = append(prices, price) + } + + if len(prices) == 0 { + return nil, fmt.Errorf("FetchBitgetRates: no valid sell ads found for %s/USDT", currency) + } + + return prices, nil +} + diff --git a/utils/external_market_test.go b/utils/external_market_test.go new file mode 100644 index 00000000..85b254d9 --- /dev/null +++ b/utils/external_market_test.go @@ -0,0 +1,321 @@ +package utils + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" +) + +func TestFetchExternalRate(t *testing.T) { + // Mock Bitget server + bitgetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var reqBody map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil || reqBody["side"] != float64(2) || reqBody["coinCode"] != "USDT" { + w.WriteHeader(http.StatusBadRequest) + return + } + + fiatCode, ok := reqBody["fiatCode"].(string) + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + + var response string + switch fiatCode { + case "NGN": + response = `{ + "code": "00000", + "msg": "success", + "data": { + "dataList": [ + {"price": "750.0", "coinCode": "USDT", "fiatCode": "NGN"}, + {"price": "755.0", "coinCode": "USDT", "fiatCode": "NGN"} + ] + } + }` + case "KES": + response = `{ + "code": "00000", + "msg": "success", + "data": { + "dataList": [ + {"price": "145.0", "coinCode": "USDT", "fiatCode": "KES"}, + {"price": "146.0", "coinCode": "USDT", "fiatCode": "KES"} + ] + } + }` + case "GHS": + response = `{ + "code": "00000", + "msg": "success", + "data": { + "dataList": [ + {"price": "15.0", "coinCode": "USDT", "fiatCode": "GHS"}, + {"price": "15.5", "coinCode": "USDT", "fiatCode": "GHS"} + ] + } + }` + case "TZS": + response = `{ + "code": "00000", + "msg": "success", + "data": { + "dataList": [ + {"price": "2700.0", "coinCode": "USDT", "fiatCode": "TZS"}, + {"price": "2750.0", "coinCode": "USDT", "fiatCode": "TZS"} + ] + } + }` + case "UGX": + response = `{ + "code": "00000", + "msg": "success", + "data": { + "dataList": [ + {"price": "3700.0", "coinCode": "USDT", "fiatCode": "UGX"}, + {"price": "3750.0", "coinCode": "USDT", "fiatCode": "UGX"} + ] + } + }` + case "XOF": + response = `{ + "code": "00000", + "msg": "success", + "data": { + "dataList": [ + {"price": "600.0", "coinCode": "USDT", "fiatCode": "XOF"}, + {"price": "610.0", "coinCode": "USDT", "fiatCode": "XOF"} + ] + } + }` + default: + response = `{"code": "80001", "msg": "invalid fiatCode", "data": {"dataList": []}}` + } + w.Write([]byte(response)) + })) + defer bitgetServer.Close() + + // Mock Quidax server (for NGN only) + quidaxServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + response := `{"data": {"ticker": {"buy": "755.00"}}}` + w.Write([]byte(response)) + })) + defer quidaxServer.Close() + + // Mock Binance server (for non-NGN currencies) + binanceServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + var reqBody map[string]interface{} + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil || reqBody["asset"] != "USDT" || reqBody["tradeType"] != "SELL" { + w.WriteHeader(http.StatusBadRequest) + return + } + + fiat, ok := reqBody["fiat"].(string) + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + + var response string + switch fiat { + case "KES": + response = `{"data":[{"adv":{"price":"145.50"}}]}` + case "GHS": + response = `{"data":[{"adv":{"price":"15.25"}}]}` + case "TZS": + response = `{"data":[{"adv":{"price":"2725.0"}}]}` + case "UGX": + response = `{"data":[{"adv":{"price":"3725.0"}}]}` + case "XOF": + response = `{"data":[{"adv":{"price":"605.0"}}]}` + default: + response = `{"data":[]}` + } + w.Write([]byte(response)) + })) + defer binanceServer.Close() + + // Test cases + tests := []struct { + name string + currency string + expectedRate decimal.Decimal + expectError bool + errorContains string + setup func() // Optional setup for edge cases + }{ + { + name: "NGN (Quidax & Bitget)", + currency: "NGN", + expectedRate: decimal.NewFromFloat(755.00), + expectError: false, + }, + { + name: "KES (Binance & Bitget)", + currency: "KES", + expectedRate: decimal.NewFromFloat(145.50), + expectError: false, + }, + { + name: "GHS (Binance & Bitget)", + currency: "GHS", + expectedRate: decimal.NewFromFloat(15.25), + expectError: false, + }, + { + name: "TZS (Binance & Bitget)", + currency: "TZS", + expectedRate: decimal.NewFromFloat(2725.0), + expectError: false, + }, + { + name: "UGX (Binance & Bitget)", + currency: "UGX", + expectedRate: decimal.NewFromFloat(3725.0), + expectError: false, + }, + { + name: "XOF (Binance & Bitget)", + currency: "XOF", + expectedRate: decimal.NewFromFloat(605.0), + expectError: false, + }, + { + name: "Unsupported currency", + currency: "USD", + expectedRate: decimal.Zero, + expectError: true, + errorContains: "currency not supported", + }, + { + name: "All APIs fail", + currency: "NGN", + expectedRate: decimal.Zero, + expectError: true, + errorContains: "no valid rates found", + setup: func() { + BitgetAPIURL = "http://invalid-url" + BinanceAPIURL = "http://invalid-url" + QuidaxAPIURL = "http://invalid-url" + }, + }, + { + name: "Bitget empty response (NGN with Quidax)", + currency: "NGN", + expectedRate: decimal.NewFromFloat(755.00), // Quidax rate only + expectError: false, + setup: func() { + BitgetAPIURL = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + response := `{"code": "00000", "msg": "success", "data": {"dataList": []}}` + w.Write([]byte(response)) + })).URL + }, + }, + { + name: "Bitget and Binance empty (KES)", + currency: "KES", + expectedRate: decimal.Zero, + expectError: true, + errorContains: "no valid rates found", + setup: func() { + BitgetAPIURL = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + response := `{"code": "00000", "msg": "success", "data": {"dataList": []}}` + w.Write([]byte(response)) + })).URL + BinanceAPIURL = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + response := `{"data":[]}` + w.Write([]byte(response)) + })).URL + }, + }, + { + name: "Bitget invalid price (KES with Binance)", + currency: "KES", + expectedRate: decimal.NewFromFloat(145.50), // Only Binance rate + expectError: false, + setup: func() { + BitgetAPIURL = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + response := `{ + "code": "00000", + "msg": "success", + "data": { + "dataList": [ + {"price": "invalid", "coinCode": "USDT", "fiatCode": "KES"} + ] + } + }` + w.Write([]byte(response)) + })).URL + }, + }, + { + name: "Binance empty response (KES with Bitget)", + currency: "KES", + expectedRate: decimal.NewFromFloat(145.5), // Median of [145.0, 146.0] + expectError: false, + setup: func() { + BinanceAPIURL = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + response := `{"data":[]}` + w.Write([]byte(response)) + })).URL + }, + }, + { + name: "Quidax fails, Bitget succeeds (NGN)", + currency: "NGN", + expectedRate: decimal.NewFromFloat(752.5), // Median of [750.0, 755.0] + expectError: false, + setup: func() { + QuidaxAPIURL = "http://invalid-url" + }, + }, + { + name: "Bitget fails, Binance succeeds (GHS)", + currency: "GHS", + expectedRate: decimal.NewFromFloat(15.25), + expectError: false, + setup: func() { + BitgetAPIURL = "http://invalid-url" + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset URLs to defaults + BitgetAPIURL = bitgetServer.URL + BinanceAPIURL = binanceServer.URL + QuidaxAPIURL = quidaxServer.URL + + // Apply custom setup if provided + if tt.setup != nil { + tt.setup() + } + + rate, err := FetchExternalRate(tt.currency) + if tt.expectError { + assert.Error(t, err, "Expected an error for %s", tt.name) + assert.Contains(t, err.Error(), tt.errorContains, "Error message mismatch for %s", tt.name) + assert.True(t, rate.Equal(decimal.Zero), "Rate should be zero on error for %s", tt.name) + } else { + assert.NoError(t, err, "Unexpected error for %s: %v", tt.name, err) + assert.Equal(t, tt.expectedRate.StringFixed(2), rate.StringFixed(2), "Rate mismatch for %s", tt.currency) + } + }) + } +} diff --git a/utils/logger/logger.go b/utils/logger/logger.go index 56872657..d05bd43a 100644 --- a/utils/logger/logger.go +++ b/utils/logger/logger.go @@ -2,8 +2,10 @@ package logger import ( "bytes" + "fmt" "os" "path/filepath" + "runtime" "strings" "time" @@ -20,7 +22,6 @@ func init() { logger.Formatter = &formatter{} config := config.ServerConfig() - if config.Environment == "production" || config.Environment == "staging" { // init sentry err := sentry.Init(sentry.ClientOptions{ @@ -71,6 +72,47 @@ func SetLogLevel(level logrus.Level) { // Fields type, used to pass to `WithFields`. type Fields logrus.Fields +// WithFields returns a new entry with the provided fields and automatically adds caller information. +func WithFields(fields Fields) *logrus.Entry { + // Get caller information (skip 1 stack frame to get the caller of WithFields) + _, file, line, ok := runtime.Caller(1) + if ok { + // Extract just the filename without the full path + _, fileName := filepath.Split(file) + + // Add caller information to fields + logrusFields := logrus.Fields(fields) + if _, exists := logrusFields["file"]; !exists { + logrusFields["File"] = fileName + } + if _, exists := logrusFields["line"]; !exists { + logrusFields["Line"] = line + } + + // Try to get function name + pc, _, _, funcOk := runtime.Caller(1) + if funcOk { + funcName := runtime.FuncForPC(pc).Name() + // Extract just the function name without the full package path + if lastDot := strings.LastIndex(funcName, "."); lastDot != -1 { + funcName = funcName[lastDot+1:] + } + if _, exists := logrusFields["function"]; !exists { + logrusFields["Function"] = funcName + } + } + + return logger.WithFields(logrusFields) + } + + return logger.WithFields(logrus.Fields(fields)) +} + +// WithField returns a new entry with the provided field and automatically adds caller information. +func WithField(key string, value interface{}) *logrus.Entry { + return WithFields(Fields{key: value}) +} + // Debugf logs a message at level Debug on the standard logger. func Debugf(format string, args ...interface{}) { if logger.Level >= logrus.DebugLevel { @@ -127,5 +169,22 @@ func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { sb.WriteString(f.prefix) sb.WriteString(entry.Message) + // Add fields to the log message if there are any + if len(entry.Data) > 0 { + sb.WriteString(" | ") + first := true + for k, v := range entry.Data { + if first { + first = false + } else { + sb.WriteString(", ") + } + sb.WriteString(k) + sb.WriteString("=") + sb.WriteString(fmt.Sprintf("%v", v)) + } + } + + sb.WriteString("\n") return sb.Bytes(), nil } diff --git a/utils/test/db.go b/utils/test/db.go index 0fef06b5..06c9d26f 100644 --- a/utils/test/db.go +++ b/utils/test/db.go @@ -200,7 +200,7 @@ func CreateTestLockPaymentOrder(overrides map[string]interface{}) (*ent.LockPaym "account_identifier": "1234567890", "account_name": "Test Account", "updatedAt": time.Now(), - "tokenID": 0, + "token_id": 0, "cancellation_reasons": []string{}, } @@ -217,7 +217,7 @@ func CreateTestLockPaymentOrder(overrides map[string]interface{}) (*ent.LockPaym payload[key] = value } - if payload["tokenID"].(int) == 0 { + if payload["token_id"].(int) == 0 { // Create test token backend, _ := SetUpTestBlockchain() token, err := CreateERC20Token(backend, map[string]interface{}{ @@ -226,7 +226,7 @@ func CreateTestLockPaymentOrder(overrides map[string]interface{}) (*ent.LockPaym if err != nil { return nil, err } - payload["tokenID"] = token.ID + payload["token_id"] = token.ID } // Create LockPaymentOrder @@ -241,7 +241,7 @@ func CreateTestLockPaymentOrder(overrides map[string]interface{}) (*ent.LockPaym SetInstitution(payload["institution"].(string)). SetAccountIdentifier(payload["account_identifier"].(string)). SetAccountName(payload["account_name"].(string)). - SetTokenID(payload["tokenID"].(int)). + SetTokenID(payload["token_id"].(int)). SetProvider(providerProfile). SetUpdatedAt(payload["updatedAt"].(time.Time)). SetCancellationReasons(payload["cancellation_reasons"].([]string)). @@ -468,7 +468,6 @@ func CreateTestProviderProfile(overrides map[string]interface{}) (*ent.ProviderP // Create ProviderProfile profile, err := db.Client.ProviderProfile. Create(). - SetID(payload["user_id"].(uuid.UUID).String()). SetTradingName(payload["trading_name"].(string)). SetHostIdentifier(payload["host_identifier"].(string)). SetProvisionMode(providerprofile.ProvisionMode(payload["provision_mode"].(string))). @@ -499,9 +498,9 @@ func AddProviderOrderTokenToProvider(overrides map[string]interface{}) (*ent.Pro "max_order_amount": decimal.NewFromFloat(1.0), "min_order_amount": decimal.NewFromFloat(1.0), "provider": nil, - "tokenID": 0, + "token_id": 0, "address": "0x1234567890123456789012345678901234567890", - "network": "polygon", + "network": "localhost", } // Apply overrides @@ -509,7 +508,7 @@ func AddProviderOrderTokenToProvider(overrides map[string]interface{}) (*ent.Pro payload[key] = value } - if payload["tokenID"].(int) == 0 { + if payload["token_id"].(int) == 0 { // Create test token backend, _ := SetUpTestBlockchain() token, err := CreateERC20Token(backend, map[string]interface{}{ @@ -518,7 +517,7 @@ func AddProviderOrderTokenToProvider(overrides map[string]interface{}) (*ent.Pro if err != nil { return nil, err } - payload["tokenID"] = token.ID + payload["token_id"] = token.ID } orderToken, err := db.Client.ProviderOrderToken. @@ -531,10 +530,19 @@ func AddProviderOrderTokenToProvider(overrides map[string]interface{}) (*ent.Pro SetFloatingConversionRate(payload["floating_conversion_rate"].(decimal.Decimal)). SetAddress(payload["address"].(string)). SetNetwork(payload["network"].(string)). - SetTokenID(payload["tokenID"].(int)). + SetTokenID(payload["token_id"].(int)). SetCurrencyID(payload["currency_id"].(uuid.UUID)). + SetRateSlippage(decimal.NewFromFloat(0.1)). Save(context.Background()) + orderToken, err = db.Client.ProviderOrderToken. + Query(). + Where(providerordertoken.IDEQ(orderToken.ID)). + WithCurrency(). + WithToken(). + WithProvider(). + Only(context.Background()) + return orderToken, err } diff --git a/utils/test/test.env b/utils/test/test.env index 63874742..2739985f 100644 --- a/utils/test/test.env +++ b/utils/test/test.env @@ -1,5 +1,5 @@ SECRET=h9wt*pasj6796jw(w8=xaje8tpi6+k2) -HOST_DOMAIN=https://example.com +SERVER_URL=https://example.com # Test Database Config diff --git a/utils/utils.go b/utils/utils.go index 2368c29f..42cf8c6c 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "fmt" "math/big" + "net/url" "reflect" "regexp" "sort" @@ -110,12 +111,9 @@ func BigMin(x, y *big.Int) *big.Int { return y } -// FormatTimestampToGMT1 formats the timestamp to GMT+1 (Africa/Lagos time zone) and returns a formatted string +// FormatTimestampToGMT1 formats the timestamp to GMT+1 (Africa/Lagos time zone) and returns a formatted string. func FormatTimestampToGMT1(timestamp time.Time) (string, error) { - loc, err := time.LoadLocation("Africa/Lagos") - if err != nil { - return "", err - } + loc := time.FixedZone("GMT+1", 1*60*60) return timestamp.In(loc).Format("January 2, 2006 at 3:04 PM"), nil } @@ -182,6 +180,15 @@ func Median(data []decimal.Decimal) decimal.Decimal { return result } +// buildQueryString constructs a query string from a map. +func BuildQueryString(params map[string]string) string { + var parts []string + for key, value := range params { + parts = append(parts, fmt.Sprintf("%s=%s", key, value)) + } + return strings.Join(parts, "&") +} + // AbsPercentageDeviation returns the absolute percentage deviation between two values func AbsPercentageDeviation(trueValue, measuredValue decimal.Decimal) decimal.Decimal { if trueValue.IsZero() { @@ -525,7 +532,14 @@ func GetTokenRateFromQueue(tokenSymbol string, orderAmount decimal.Decimal, fiat } parts := strings.Split(providerData, ":") if len(parts) != 5 { - logger.Errorf("utils.GetTokenRateFromQueue.InvalidProviderData: %v", providerData) + logger.WithFields(logger.Fields{ + "Error": fmt.Sprintf("%v", err), + "ProviderData": providerData, + "Token": tokenSymbol, + "Currency": fiatCurrency, + "MinAmount": minAmount, + "MaxAmount": maxAmount, + }).Errorf("GetTokenRate.InvalidProviderData: %v", providerData) continue } @@ -569,18 +583,41 @@ func GetTokenRateFromQueue(tokenSymbol string, orderAmount decimal.Decimal, fiat } // GetInstitutionByCode returns the institution for a given institution code -func GetInstitutionByCode(ctx context.Context, institutionCode string) (*ent.Institution, error) { - institution, err := storage.Client.Institution. +func GetInstitutionByCode(ctx context.Context, institutionCode string, enabledFiatCurrency bool) (*ent.Institution, error) { + institutionQuery := storage.Client.Institution. Query(). - Where(institution.CodeEQ(institutionCode)). - WithFiatCurrency( + Where(institution.CodeEQ(institutionCode)) + + if enabledFiatCurrency { + institutionQuery = institutionQuery.WithFiatCurrency( func(fcq *ent.FiatCurrencyQuery) { fcq.Where(fiatcurrency.IsEnabledEQ(true)) }, - ). - Only(ctx) + ) + } else { + institutionQuery = institutionQuery.WithFiatCurrency() + } + + institution, err := institutionQuery.Only(ctx) if err != nil { return nil, err } return institution, nil } + +// Helper function to validate HTTPS URL +func IsValidHttpsUrl(urlStr string) bool { + // Check if URL starts with https:// + if !strings.HasPrefix(strings.ToLower(urlStr), "https://") { + return false + } + + // Parse URL to ensure it's valid + parsedUrl, err := url.Parse(urlStr) + if err != nil { + return false + } + + // Verify scheme is https and host is present + return parsedUrl.Scheme == "https" && parsedUrl.Host != "" +}