diff --git a/cmd/db_test.go b/cmd/db_test.go index 9e5f09c01..4a4855660 100644 --- a/cmd/db_test.go +++ b/cmd/db_test.go @@ -6,14 +6,14 @@ import ( "strings" "testing" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" + "github.com/stellar/go/keypair" "github.com/stellar/go/network" "github.com/stellar/go/support/log" "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "github.com/stellar/stellar-disbursement-platform-backend/internal/db" "github.com/stellar/stellar-disbursement-platform-backend/internal/db/dbtest" - "github.com/stellar/stellar-disbursement-platform-backend/internal/services" - "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -158,12 +158,12 @@ func Test_DatabaseCommand_db_setup_for_network(t *testing.T) { testnetUSDCIssuer := keypair.MustRandom().Address() data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", testnetUSDCIssuer) - assets, err := models.Assets.GetAll(ctx) + actualAssets, err := models.Assets.GetAll(ctx) require.NoError(t, err) - assert.Len(t, assets, 1) - assert.Equal(t, "USDC", assets[0].Code) - assert.Equal(t, testnetUSDCIssuer, assets[0].Issuer) + assert.Len(t, actualAssets, 1) + assert.Equal(t, "USDC", actualAssets[0].Code) + assert.Equal(t, testnetUSDCIssuer, actualAssets[0].Issuer) // Wallets data.CreateWalletFixture(t, ctx, dbConnectionPool, "Vibrant Assist", "https://vibrantapp.com", "api-dev.vibrantapp.com", "https://vibrantapp.com/sdp-dev") @@ -196,15 +196,14 @@ func Test_DatabaseCommand_db_setup_for_network(t *testing.T) { require.NoError(t, err) // Validating assets - assets, err = models.Assets.GetAll(ctx) + actualAssets, err = models.Assets.GetAll(ctx) require.NoError(t, err) - assert.Len(t, assets, 2) - assert.Equal(t, "USDC", assets[0].Code) - assert.NotEqual(t, testnetUSDCIssuer, assets[0].Issuer) - assert.Equal(t, services.DefaultAssetsNetworkMap[utils.PubnetNetworkType]["USDC"], assets[0].Issuer) - assert.Equal(t, "XLM", assets[1].Code) - assert.Empty(t, assets[1].Issuer) + require.Len(t, actualAssets, 2) + require.Equal(t, assets.USDCAssetPubnet.Code, actualAssets[0].Code) + require.Equal(t, assets.USDCAssetPubnet.Issuer, actualAssets[0].Issuer) + require.Equal(t, assets.XLMAsset.Code, actualAssets[1].Code) + require.Empty(t, assets.XLMAsset.Issuer) // Validating wallets wallets, err = models.Wallets.GetAll(ctx) @@ -238,7 +237,7 @@ func Test_DatabaseCommand_db_setup_for_network(t *testing.T) { expectedLogs := []string{ "updating/inserting assets for the 'pubnet' network", "Code: USDC", - fmt.Sprintf("Issuer: %s", services.DefaultAssetsNetworkMap[utils.PubnetNetworkType]["USDC"]), + fmt.Sprintf("Issuer: %s", assets.USDCAssetPubnet.Issuer), "Code: XLM", "Issuer: ", "updating/inserting wallets for the 'pubnet' network", diff --git a/internal/data/disbursements.go b/internal/data/disbursements.go index cda54077b..2e8bd413d 100644 --- a/internal/data/disbursements.go +++ b/internal/data/disbursements.go @@ -58,6 +58,15 @@ const ( VerificationFieldNationalID VerificationField = "NATIONAL_ID_NUMBER" ) +// GetAllVerificationFields returns all verification fields +func GetAllVerificationFields() []VerificationField { + return []VerificationField{ + VerificationFieldDateOfBirth, + VerificationFieldPin, + VerificationFieldNationalID, + } +} + type DisbursementStatusHistoryEntry struct { UserID string `json:"user_id"` Status DisbursementStatus `json:"status"` @@ -77,9 +86,9 @@ var ( func (d *DisbursementModel) Insert(ctx context.Context, disbursement *Disbursement) (string, error) { const q = ` INSERT INTO - disbursements (name, status, status_history, wallet_id, asset_id, country_code) + disbursements (name, status, status_history, wallet_id, asset_id, country_code, verification_field) VALUES - ($1, $2, $3, $4, $5, $6) + ($1, $2, $3, $4, $5, $6, $7) RETURNING id ` var newId string @@ -90,6 +99,7 @@ func (d *DisbursementModel) Insert(ctx context.Context, disbursement *Disburseme disbursement.Wallet.ID, disbursement.Asset.ID, disbursement.Country.Code, + disbursement.VerificationField, ) if err != nil { // check if the error is a duplicate key error @@ -130,6 +140,7 @@ func (d *DisbursementModel) Get(ctx context.Context, sqlExec db.SQLExecuter, id d.file_content, d.created_at, d.updated_at, + d.verification_field, w.id as "wallet.id", w.name as "wallet.name", w.homepage as "wallet.homepage", @@ -180,6 +191,7 @@ func (d *DisbursementModel) GetByName(ctx context.Context, sqlExec db.SQLExecute d.file_content, d.created_at, d.updated_at, + d.verification_field, w.id as "wallet.id", w.name as "wallet.name", w.homepage as "wallet.homepage", @@ -319,6 +331,7 @@ func (d *DisbursementModel) GetAll(ctx context.Context, sqlExec db.SQLExecuter, d.verification_field, d.created_at, d.updated_at, + d.verification_field, COALESCE(d.file_name, '') as file_name, w.id as "wallet.id", w.name as "wallet.name", diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go index 91c933da2..ec3d06e3a 100644 --- a/internal/data/disbursements_test.go +++ b/internal/data/disbursements_test.go @@ -37,9 +37,10 @@ func Test_DisbursementModelInsert(t *testing.T) { UserID: "user1", }, }, - Asset: asset, - Country: country, - Wallet: wallet, + Asset: asset, + Country: country, + Wallet: wallet, + VerificationField: VerificationFieldDateOfBirth, } t.Run("returns error when disbursement already exists is not found", func(t *testing.T) { @@ -67,6 +68,7 @@ func Test_DisbursementModelInsert(t *testing.T) { assert.Equal(t, 1, len(actual.StatusHistory)) assert.Equal(t, DraftDisbursementStatus, actual.StatusHistory[0].Status) assert.Equal(t, "user1", actual.StatusHistory[0].UserID) + assert.Equal(t, VerificationFieldDateOfBirth, actual.VerificationField) }) } diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go index cff9c5e4b..b62a9e43d 100644 --- a/internal/data/fixtures.go +++ b/internal/data/fixtures.go @@ -514,6 +514,10 @@ func CreateDisbursementFixture(t *testing.T, ctx context.Context, sqlExec db.SQL if d.Country == nil { d.Country = GetCountryFixture(t, ctx, sqlExec, FixtureCountryUKR) } + if d.VerificationField == "" { + d.VerificationField = VerificationFieldDateOfBirth + } + // insert disbursement if d.StatusHistory == nil { d.StatusHistory = []DisbursementStatusHistoryEntry{{ @@ -583,6 +587,10 @@ func CreateDraftDisbursementFixture(t *testing.T, ctx context.Context, sqlExec d insert.Status = DraftDisbursementStatus } + if insert.VerificationField == "" { + insert.VerificationField = VerificationFieldDateOfBirth + } + id, err := model.Insert(ctx, &insert) require.NoError(t, err) diff --git a/internal/integrationtests/integration_tests.go b/internal/integrationtests/integration_tests.go index 6c886aa56..17134a8c8 100644 --- a/internal/integrationtests/integration_tests.go +++ b/internal/integrationtests/integration_tests.go @@ -121,10 +121,11 @@ func (it *IntegrationTestsService) StartIntegrationTests(ctx context.Context, op log.Ctx(ctx).Info("Creating disbursement using server API") disbursement, err := it.serverAPI.CreateDisbursement(ctx, authToken, &httphandler.PostDisbursementRequest{ - Name: opts.DisbursementName, - CountryCode: "USA", - WalletID: wallet.ID, - AssetID: asset.ID, + Name: opts.DisbursementName, + CountryCode: "USA", + WalletID: wallet.ID, + AssetID: asset.ID, + VerificationField: data.VerificationFieldDateOfBirth, }) if err != nil { return fmt.Errorf("error creating disbursement: %w", err) diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go index 1b9af8a78..780b8568b 100644 --- a/internal/serve/httphandler/disbursement_handler.go +++ b/internal/serve/httphandler/disbursement_handler.go @@ -33,10 +33,11 @@ type DisbursementHandler struct { } type PostDisbursementRequest struct { - Name string `json:"name"` - CountryCode string `json:"country_code"` - WalletID string `json:"wallet_id"` - AssetID string `json:"asset_id"` + Name string `json:"name"` + CountryCode string `json:"country_code"` + WalletID string `json:"wallet_id"` + AssetID string `json:"asset_id"` + VerificationField data.VerificationField `json:"verification_field"` } type PatchDisbursementStatusRequest struct { @@ -52,9 +53,7 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req return } - // validate request - v := validators.NewValidator() - + v := validators.NewDisbursementRequestValidator(disbursementRequest.VerificationField) v.Check(disbursementRequest.Name != "", "name", "name is required") v.Check(disbursementRequest.CountryCode != "", "country_code", "country_code is required") v.Check(disbursementRequest.WalletID != "", "wallet_id", "wallet_id is required") @@ -65,6 +64,13 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req return } + verificationField := v.ValidateAndGetVerificationType() + + if v.HasErrors() { + httperror.BadRequest("Verification field invalid", err, v.Errors).Render(w) + return + } + ctx := r.Context() wallet, err := d.Models.Wallets.Get(ctx, disbursementRequest.WalletID) if err != nil { @@ -107,9 +113,10 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req Status: data.DraftDisbursementStatus, UserID: user.ID, }}, - Wallet: wallet, - Asset: asset, - Country: country, + Wallet: wallet, + Asset: asset, + Country: country, + VerificationField: verificationField, } newId, err := d.Models.Disbursements.Insert(ctx, &disbursement) diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go index 23d88159a..b3c42b004 100644 --- a/internal/serve/httphandler/disbursement_handler_test.go +++ b/internal/serve/httphandler/disbursement_handler_test.go @@ -97,7 +97,8 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { { "wallet_id": "aab4a4a9-2493-4f37-9741-01d5bd31d68b", "asset_id": "61dbfa89-943a-413c-b862-a2177384d321", - "country_code": "UKR" + "country_code": "UKR", + "verification_field": "date_of_birth" }` want := ` @@ -116,7 +117,8 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { { "name": "My New Disbursement name 5", "asset_id": "61dbfa89-943a-413c-b862-a2177384d321", - "country_code": "UKR" + "country_code": "UKR", + "verification_field": "date_of_birth" }` want := `{"error":"Request invalid", "extras": {"wallet_id": "wallet_id is required"}}` @@ -129,7 +131,8 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { { "name": "My New Disbursement name 5", "wallet_id": "aab4a4a9-2493-4f37-9741-01d5bd31d68b", - "country_code": "UKR" + "country_code": "UKR", + "verification_field": "date_of_birth" }` want := `{"error":"Request invalid", "extras": {"asset_id": "asset_id is required"}}` @@ -142,7 +145,8 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { { "name": "My New Disbursement name 5", "wallet_id": "aab4a4a9-2493-4f37-9741-01d5bd31d68b", - "asset_id": "61dbfa89-943a-413c-b862-a2177384d321" + "asset_id": "61dbfa89-943a-413c-b862-a2177384d321", + "verification_field": "date_of_birth" }` want := `{"error":"Request invalid", "extras": {"country_code": "country_code is required"}}` @@ -150,12 +154,27 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { assertPOSTResponse(t, ctx, handler, method, url, requestBody, want, http.StatusBadRequest) }) - t.Run("returns error when wallet_id is not valid", func(t *testing.T) { + t.Run("returns error when no verification field is provided", func(t *testing.T) { requestBody, err := json.Marshal(PostDisbursementRequest{ Name: "disbursement 1", CountryCode: country.Code, AssetID: asset.ID, - WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b", + WalletID: enabledWallet.ID, + }) + require.NoError(t, err) + + want := `{"error":"Verification field invalid", "extras": {"verification_field": "invalid parameter. valid values are: DATE_OF_BIRTH, PIN, NATIONAL_ID_NUMBER"}}` + + assertPOSTResponse(t, ctx, handler, method, url, string(requestBody), want, http.StatusBadRequest) + }) + + t.Run("returns error when wallet_id is not valid", func(t *testing.T) { + requestBody, err := json.Marshal(PostDisbursementRequest{ + Name: "disbursement 1", + CountryCode: country.Code, + AssetID: asset.ID, + WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b", + VerificationField: data.VerificationFieldDateOfBirth, }) require.NoError(t, err) @@ -167,10 +186,11 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { t.Run("returns error when wallet is not enabled", func(t *testing.T) { data.EnableOrDisableWalletFixtures(t, ctx, dbConnectionPool, false, disabledWallet.ID) requestBody, err := json.Marshal(PostDisbursementRequest{ - Name: "disbursement 1", - CountryCode: country.Code, - AssetID: asset.ID, - WalletID: disabledWallet.ID, + Name: "disbursement 1", + CountryCode: country.Code, + AssetID: asset.ID, + WalletID: disabledWallet.ID, + VerificationField: data.VerificationFieldDateOfBirth, }) require.NoError(t, err) @@ -181,10 +201,11 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { t.Run("returns error when asset_id is not valid", func(t *testing.T) { requestBody, err := json.Marshal(PostDisbursementRequest{ - Name: "disbursement 1", - CountryCode: country.Code, - AssetID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b", - WalletID: enabledWallet.ID, + Name: "disbursement 1", + CountryCode: country.Code, + AssetID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b", + WalletID: enabledWallet.ID, + VerificationField: data.VerificationFieldDateOfBirth, }) require.NoError(t, err) @@ -195,10 +216,11 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { t.Run("returns error when country_code is not valid", func(t *testing.T) { requestBody, err := json.Marshal(PostDisbursementRequest{ - Name: "disbursement 1", - CountryCode: "AAA", - AssetID: asset.ID, - WalletID: enabledWallet.ID, + Name: "disbursement 1", + CountryCode: "AAA", + AssetID: asset.ID, + WalletID: enabledWallet.ID, + VerificationField: data.VerificationFieldDateOfBirth, }) require.NoError(t, err) @@ -217,10 +239,11 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { mMonitorService.On("MonitorCounters", monitor.DisbursementsCounterTag, labels.ToMap()).Return(nil).Once() requestBody, err := json.Marshal(PostDisbursementRequest{ - Name: "disbursement 1", - CountryCode: country.Code, - AssetID: asset.ID, - WalletID: enabledWallet.ID, + Name: "disbursement 1", + CountryCode: country.Code, + AssetID: asset.ID, + WalletID: enabledWallet.ID, + VerificationField: data.VerificationFieldDateOfBirth, }) require.NoError(t, err) @@ -238,10 +261,11 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { expectedName := "disbursement 2" requestBody, err := json.Marshal(PostDisbursementRequest{ - Name: expectedName, - CountryCode: country.Code, - AssetID: asset.ID, - WalletID: enabledWallet.ID, + Name: expectedName, + CountryCode: country.Code, + AssetID: asset.ID, + WalletID: enabledWallet.ID, + VerificationField: data.VerificationFieldDateOfBirth, }) require.NoError(t, err) @@ -1055,6 +1079,9 @@ func Test_DisbursementHandler_PatchDisbursementStatus(t *testing.T) { r := chi.NewRouter() r.Patch("/disbursements/{id}/status", handler.PatchDisbursementStatus) + disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{}) + require.NotNil(t, disbursement) + readyStatusHistory := []data.DisbursementStatusHistoryEntry{ { Status: data.DraftDisbursementStatus, diff --git a/internal/serve/httphandler/receiver_handler.go b/internal/serve/httphandler/receiver_handler.go index 3f6c3d896..db87e4719 100644 --- a/internal/serve/httphandler/receiver_handler.go +++ b/internal/serve/httphandler/receiver_handler.go @@ -133,3 +133,8 @@ func (rh ReceiverHandler) GetReceivers(w http.ResponseWriter, r *http.Request) { httpjson.RenderStatus(w, http.StatusOK, httpResponse, httpjson.JSON) } + +// GetReceiverVerification returns a list of verification types +func (rh ReceiverHandler) GetReceiverVerificationTypes(w http.ResponseWriter, r *http.Request) { + httpjson.Render(w, data.GetAllVerificationFields(), httpjson.JSON) +} diff --git a/internal/serve/httphandler/receiver_handler_test.go b/internal/serve/httphandler/receiver_handler_test.go index a846f2dc4..d75f6b08b 100644 --- a/internal/serve/httphandler/receiver_handler_test.go +++ b/internal/serve/httphandler/receiver_handler_test.go @@ -1616,3 +1616,24 @@ func Test_ReceiverHandler_BuildReceiversResponse(t *testing.T) { err = dbTx.Commit() require.NoError(t, err) } + +func Test_ReceiverHandler_GetReceiverVerificatioTypes(t *testing.T) { + handler := &ReceiverHandler{} + + rr := httptest.NewRecorder() + req, err := http.NewRequest(http.MethodGet, "/receivers/verification-types", nil) + require.NoError(t, err) + http.HandlerFunc(handler.GetReceiverVerificationTypes).ServeHTTP(rr, req) + + resp := rr.Result() + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + defer resp.Body.Close() + expectedBody := `[ + "DATE_OF_BIRTH", + "PIN", + "NATIONAL_ID_NUMBER" + ]` + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.JSONEq(t, expectedBody, string(respBody)) +} diff --git a/internal/serve/serve.go b/internal/serve/serve.go index afe12ccae..e97c1384f 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -262,6 +262,9 @@ func handleHTTP(o ServeOptions) *chi.Mux { r.With(middleware.AnyRoleMiddleware(authManager, data.OwnerUserRole, data.FinancialControllerUserRole)). Get("/{id}", receiversHandler.GetReceiver) + r.With(middleware.AnyRoleMiddleware(authManager, data.GetAllRoles()...)). + Get("/verification-types", receiversHandler.GetReceiverVerificationTypes) + updateReceiverHandler := httphandler.UpdateReceiverHandler{Models: o.Models, DBConnectionPool: o.dbConnectionPool} r.With(middleware.AnyRoleMiddleware(authManager, data.OwnerUserRole, data.FinancialControllerUserRole)). Patch("/{id}", updateReceiverHandler.UpdateReceiver) diff --git a/internal/serve/serve_test.go b/internal/serve/serve_test.go index f4638e81a..7ef065e57 100644 --- a/internal/serve/serve_test.go +++ b/internal/serve/serve_test.go @@ -294,6 +294,7 @@ func Test_handleHTTP_authenticatedEndpoints(t *testing.T) { {http.MethodGet, "/receivers/1234"}, {http.MethodPatch, "/receivers/1234"}, {http.MethodPatch, "/receivers/wallets/1234"}, + {http.MethodGet, "/receivers/verification-types"}, // Countries {http.MethodGet, "/countries"}, // Assets diff --git a/internal/serve/validators/disbursement_instructions_validator.go b/internal/serve/validators/disbursement_instructions_validator.go index e3d2e6149..badcc0ea3 100644 --- a/internal/serve/validators/disbursement_instructions_validator.go +++ b/internal/serve/validators/disbursement_instructions_validator.go @@ -5,9 +5,16 @@ import ( "strings" "time" + "github.com/stellar/go/support/log" + "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" +) - "github.com/stellar/stellar-disbursement-platform-backend/internal/data" +const ( + VERIFICATION_FIELD_PIN_MIN_LENGTH = 4 + VERIFICATION_FIELD_PIN_MAX_LENGTH = 8 + + VERIFICATION_FIELD_MAX_ID_LENGTH = 50 ) type DisbursementInstructionsValidator struct { @@ -46,5 +53,15 @@ func (iv *DisbursementInstructionsValidator) ValidateInstruction(instruction *da // check if date of birth is in the past iv.Check(dob.Before(time.Now()), fmt.Sprintf("line %d - birthday", lineNumber), "date of birth cannot be in the future") + } else if iv.verificationField == data.VerificationFieldPin { + if len(verification) < VERIFICATION_FIELD_PIN_MIN_LENGTH || len(verification) > VERIFICATION_FIELD_PIN_MAX_LENGTH { + iv.addError(fmt.Sprintf("line %d - pin", lineNumber), "invalid pin. Cannot have less than 4 or more than 8 characters in pin") + } + } else if iv.verificationField == data.VerificationFieldNationalID { + if len(verification) > VERIFICATION_FIELD_MAX_ID_LENGTH { + iv.addError(fmt.Sprintf("line %d - national id", lineNumber), "invalid national id. Cannot have more than 50 characters in national id") + } + } else { + log.Warnf("Verification field %v is not being validated for ValidateReceiver", iv) } } diff --git a/internal/serve/validators/disbursement_instructions_validator_test.go b/internal/serve/validators/disbursement_instructions_validator_test.go index ebb282a13..a98f6694c 100644 --- a/internal/serve/validators/disbursement_instructions_validator_test.go +++ b/internal/serve/validators/disbursement_instructions_validator_test.go @@ -9,11 +9,12 @@ import ( func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing.T) { tests := []struct { - name string - actual *data.DisbursementInstruction - lineNumber int - hasErrors bool - expectedErrors map[string]interface{} + name string + actual *data.DisbursementInstruction + lineNumber int + verificationField data.VerificationField + hasErrors bool + expectedErrors map[string]interface{} }{ { name: "valid record", @@ -23,8 +24,9 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing Amount: "100.5", VerificationValue: "1990-01-01", }, - lineNumber: 1, - hasErrors: false, + lineNumber: 1, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: false, }, { name: "empty phone number", @@ -33,17 +35,19 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing Amount: "100.5", VerificationValue: "1990-01-01", }, - lineNumber: 2, - hasErrors: true, + lineNumber: 2, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: true, expectedErrors: map[string]interface{}{ "line 2 - phone": "phone cannot be empty", }, }, { - name: "empty phone, id, amount and birthday", - actual: &data.DisbursementInstruction{}, - lineNumber: 2, - hasErrors: true, + name: "empty phone, id, amount and birthday", + actual: &data.DisbursementInstruction{}, + lineNumber: 2, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: true, expectedErrors: map[string]interface{}{ "line 2 - amount": "invalid amount. Amount must be a positive number", "line 2 - birthday": "invalid date of birth format. Correct format: 1990-01-01", @@ -59,8 +63,9 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing Amount: "100.5", VerificationValue: "1990-01-01", }, - lineNumber: 2, - hasErrors: true, + lineNumber: 2, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: true, expectedErrors: map[string]interface{}{ "line 2 - phone": "invalid phone format. Correct format: +380445555555", }, @@ -73,8 +78,9 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing Amount: "100.5USDC", VerificationValue: "1990-01-01", }, - lineNumber: 3, - hasErrors: true, + lineNumber: 3, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - amount": "invalid amount. Amount must be a positive number", }, @@ -87,8 +93,9 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing Amount: "-100.5", VerificationValue: "1990-01-01", }, - lineNumber: 3, - hasErrors: true, + lineNumber: 3, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - amount": "invalid amount. Amount must be a positive number", }, @@ -101,8 +108,9 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing Amount: "100.5", VerificationValue: "1990/01/01", }, - lineNumber: 3, - hasErrors: true, + lineNumber: 3, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - birthday": "invalid date of birth format. Correct format: 1990-01-01", }, @@ -115,17 +123,87 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing Amount: "100.5", VerificationValue: "2090-01-01", }, - lineNumber: 3, - hasErrors: true, + lineNumber: 3, + verificationField: data.VerificationFieldDateOfBirth, + hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - birthday": "date of birth cannot be in the future", }, }, + { + name: "valid pin", + actual: &data.DisbursementInstruction{ + Phone: "+380445555555", + ID: "123456789", + Amount: "100.5", + VerificationValue: "1234", + }, + lineNumber: 3, + verificationField: data.VerificationFieldPin, + hasErrors: false, + }, + { + name: "invalid pin - less than 4 characters", + actual: &data.DisbursementInstruction{ + Phone: "+380445555555", + ID: "123456789", + Amount: "100.5", + VerificationValue: "123", + }, + lineNumber: 3, + verificationField: data.VerificationFieldPin, + hasErrors: true, + expectedErrors: map[string]interface{}{ + "line 3 - pin": "invalid pin. Cannot have less than 4 or more than 8 characters in pin", + }, + }, + { + name: "invalid pin - more than 8 characters", + actual: &data.DisbursementInstruction{ + Phone: "+380445555555", + ID: "123456789", + Amount: "100.5", + VerificationValue: "123456789", + }, + lineNumber: 3, + verificationField: data.VerificationFieldPin, + hasErrors: true, + expectedErrors: map[string]interface{}{ + "line 3 - pin": "invalid pin. Cannot have less than 4 or more than 8 characters in pin", + }, + }, + { + name: "valid national id", + actual: &data.DisbursementInstruction{ + Phone: "+380445555555", + ID: "123456789", + Amount: "100.5", + VerificationValue: "ABCD123", + }, + lineNumber: 3, + verificationField: data.VerificationFieldNationalID, + hasErrors: false, + }, + { + name: "invalid national - more than 50 characters", + actual: &data.DisbursementInstruction{ + Phone: "+380445555555", + ID: "123456789", + Amount: "100.5", + VerificationValue: "6UZMB56FWTKV4U0PJ21TBR6VOQVYSGIMZG2HW2S0L7EK5K83W78", + }, + lineNumber: 3, + verificationField: data.VerificationFieldNationalID, + hasErrors: true, + expectedErrors: map[string]interface{}{ + "line 3 - national id": "invalid national id. Cannot have more than 50 characters in national id", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - iv := NewDisbursementInstructionsValidator(data.VerificationFieldDateOfBirth) + iv := NewDisbursementInstructionsValidator(tt.verificationField) iv.ValidateInstruction(tt.actual, tt.lineNumber) if tt.hasErrors { diff --git a/internal/serve/validators/disbursement_request_validator.go b/internal/serve/validators/disbursement_request_validator.go new file mode 100644 index 000000000..7bbcc1c6d --- /dev/null +++ b/internal/serve/validators/disbursement_request_validator.go @@ -0,0 +1,26 @@ +package validators + +import "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + +type DisbursementRequestValidator struct { + verificationField data.VerificationField + *Validator +} + +func NewDisbursementRequestValidator(verificationField data.VerificationField) *DisbursementRequestValidator { + return &DisbursementRequestValidator{ + verificationField: verificationField, + Validator: NewValidator(), + } +} + +// ValidateAndGetVerificationType validates if the verification type field is a valid value. +func (dv *DisbursementRequestValidator) ValidateAndGetVerificationType() data.VerificationField { + switch dv.verificationField { + case data.VerificationFieldDateOfBirth, data.VerificationFieldPin, data.VerificationFieldNationalID: + return dv.verificationField + default: + dv.Check(false, "verification_field", "invalid parameter. valid values are: DATE_OF_BIRTH, PIN, NATIONAL_ID_NUMBER") + return "" + } +} diff --git a/internal/serve/validators/disbursement_request_validator_test.go b/internal/serve/validators/disbursement_request_validator_test.go new file mode 100644 index 000000000..b2e326c47 --- /dev/null +++ b/internal/serve/validators/disbursement_request_validator_test.go @@ -0,0 +1,32 @@ +package validators + +import ( + "testing" + + "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + "github.com/stretchr/testify/assert" +) + +func Test_DisbursementRequestValidator_ValidateAndGetVerificationType(t *testing.T) { + t.Run("Valid verification type", func(t *testing.T) { + validField := []data.VerificationField{ + data.VerificationFieldDateOfBirth, + data.VerificationFieldPin, + data.VerificationFieldNationalID, + } + for _, field := range validField { + validator := NewDisbursementRequestValidator(field) + assert.Equal(t, field, validator.ValidateAndGetVerificationType()) + } + }) + + t.Run("Invalid verification type", func(t *testing.T) { + field := data.VerificationField("field") + validator := NewDisbursementRequestValidator(field) + + actual := validator.ValidateAndGetVerificationType() + assert.Empty(t, actual) + assert.Equal(t, 1, len(validator.Errors)) + assert.Equal(t, "invalid parameter. valid values are: DATE_OF_BIRTH, PIN, NATIONAL_ID_NUMBER", validator.Errors["verification_field"]) + }) +} diff --git a/internal/services/assets/assets_pubnet.go b/internal/services/assets/assets_pubnet.go new file mode 100644 index 000000000..c3472fb86 --- /dev/null +++ b/internal/services/assets/assets_pubnet.go @@ -0,0 +1,24 @@ +package assets + +import "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + +// USDCAssetCode is the code for the USDC asset for pubnet and testnet +const USDCAssetCode = "USDC" + +// XLMAssetCode is the code for the XLM asset for pubnet and testnet +const XLMAssetCode = "XLM" + +// USDCAssetIssuerPubnet is the issuer for the USDC asset for pubnet +const USDCAssetIssuerPubnet = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + +// USDCAssetPubnet is the USDC asset for pubnet +var USDCAssetPubnet = data.Asset{ + Code: USDCAssetCode, + Issuer: USDCAssetIssuerPubnet, +} + +// XLMAsset is the XLM asset for pubnet +var XLMAsset = data.Asset{ + Code: XLMAssetCode, + Issuer: "", +} diff --git a/internal/services/assets/assets_testnet.go b/internal/services/assets/assets_testnet.go new file mode 100644 index 000000000..5dfb4ab0b --- /dev/null +++ b/internal/services/assets/assets_testnet.go @@ -0,0 +1,12 @@ +package assets + +import "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + +// USDCAssetIssuerTestnet is the issuer for the USDC asset for testnet +const USDCAssetIssuerTestnet = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5" + +// USDCAssetTestnet is the USDC asset for testnet +var USDCAssetTestnet = data.Asset{ + Code: USDCAssetCode, + Issuer: USDCAssetIssuerTestnet, +} diff --git a/internal/services/setup_assets_for_network_service.go b/internal/services/setup_assets_for_network_service.go index b608b40d6..3e0529e1c 100644 --- a/internal/services/setup_assets_for_network_service.go +++ b/internal/services/setup_assets_for_network_service.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" + "github.com/lib/pq" "github.com/stellar/go/support/log" "github.com/stellar/stellar-disbursement-platform-backend/internal/data" @@ -12,17 +14,11 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" ) -type AssetsNetworkMapType map[utils.NetworkType]map[string]string +type AssetsNetworkMapType map[utils.NetworkType][]data.Asset var DefaultAssetsNetworkMap = AssetsNetworkMapType{ - utils.PubnetNetworkType: { - "USDC": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", - "XLM": "", - }, - utils.TestnetNetworkType: { - "USDC": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - "XLM": "", - }, + utils.PubnetNetworkType: []data.Asset{assets.USDCAssetPubnet, assets.XLMAsset}, + utils.TestnetNetworkType: []data.Asset{assets.USDCAssetTestnet, assets.XLMAsset}, } // SetupAssetsForProperNetwork updates and inserts assets for the given Network Passphrase (`network`). So it avoids the application having @@ -40,11 +36,11 @@ func SetupAssetsForProperNetwork(ctx context.Context, dbConnectionPool db.DBConn separator := strings.Repeat("-", 20) buf := new(strings.Builder) buf.WriteString("assets' code that will be updated or inserted:\n\n") - for code, issuer := range assets { - codes = append(codes, code) - issuers = append(issuers, issuer) + for _, asset := range assets { + codes = append(codes, asset.Code) + issuers = append(issuers, asset.Issuer) - buf.WriteString(fmt.Sprintf("Code: %s\n%s\n\n", code, separator)) + buf.WriteString(fmt.Sprintf("Code: %s\n%s\n\n", asset.Code, separator)) } log.Ctx(ctx).Info(buf.String()) diff --git a/internal/services/setup_assets_for_network_service_test.go b/internal/services/setup_assets_for_network_service_test.go index 14bd3620f..e809e38fd 100644 --- a/internal/services/setup_assets_for_network_service_test.go +++ b/internal/services/setup_assets_for_network_service_test.go @@ -87,9 +87,9 @@ func Test_SetupAssetsForProperNetwork(t *testing.T) { assert.Equal(t, pubnetEUROCIssuer, assets[0].Issuer) assetsNetworkMap := AssetsNetworkMapType{ - utils.TestnetNetworkType: { - "EUROC": testnetEUROCIssuer, - "USDC": testnetUSDCIssuer, + utils.TestnetNetworkType: []data.Asset{ + {Code: "EUROC", Issuer: testnetEUROCIssuer}, + {Code: "USDC", Issuer: testnetUSDCIssuer}, }, } @@ -146,9 +146,9 @@ func Test_SetupAssetsForProperNetwork(t *testing.T) { assert.Equal(t, testnetEUROCIssuer, assets[1].Issuer) assetsNetworkMap := AssetsNetworkMapType{ - utils.PubnetNetworkType: { - "EUROC": pubnetEUROCIssuer, - "USDC": pubnetUSDCIssuer, + utils.PubnetNetworkType: []data.Asset{ + {Code: "EUROC", Issuer: pubnetEUROCIssuer}, + {Code: "USDC", Issuer: pubnetUSDCIssuer}, }, } diff --git a/internal/services/wallets/wallets_pubnet.go b/internal/services/wallets/wallets_pubnet.go index 50214b892..772c8b6e8 100644 --- a/internal/services/wallets/wallets_pubnet.go +++ b/internal/services/wallets/wallets_pubnet.go @@ -1,6 +1,9 @@ package wallets -import "github.com/stellar/stellar-disbursement-platform-backend/internal/data" +import ( + "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" +) var PubnetWallets = []data.Wallet{ { @@ -9,10 +12,7 @@ var PubnetWallets = []data.Wallet{ DeepLinkSchema: "https://vibrantapp.com/sdp", SEP10ClientDomain: "api.vibrantapp.com", Assets: []data.Asset{ - { - Code: "USDC", - Issuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", - }, + assets.USDCAssetPubnet, }, }, { @@ -21,10 +21,7 @@ var PubnetWallets = []data.Wallet{ DeepLinkSchema: "https://vibrantapp.com/sdp-rc", SEP10ClientDomain: "vibrantapp.com", Assets: []data.Asset{ - { - Code: "USDC", - Issuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", - }, + assets.USDCAssetPubnet, }, }, { @@ -33,10 +30,7 @@ var PubnetWallets = []data.Wallet{ DeepLinkSchema: "https://freedom-public-uat.bpventures.us/disbursement/create", SEP10ClientDomain: "freedom-public-uat.bpventures.us", Assets: []data.Asset{ - { - Code: "USDC", - Issuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", - }, + assets.USDCAssetPubnet, }, }, // { diff --git a/internal/services/wallets/wallets_testnet.go b/internal/services/wallets/wallets_testnet.go index 79231e975..bfde96118 100644 --- a/internal/services/wallets/wallets_testnet.go +++ b/internal/services/wallets/wallets_testnet.go @@ -1,6 +1,9 @@ package wallets -import "github.com/stellar/stellar-disbursement-platform-backend/internal/data" +import ( + "github.com/stellar/stellar-disbursement-platform-backend/internal/data" + "github.com/stellar/stellar-disbursement-platform-backend/internal/services/assets" +) var TestnetWallets = []data.Wallet{ { @@ -9,14 +12,8 @@ var TestnetWallets = []data.Wallet{ DeepLinkSchema: "https://demo-wallet.stellar.org", SEP10ClientDomain: "demo-wallet-server.stellar.org", Assets: []data.Asset{ - { - Code: "USDC", - Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - }, - { - Code: "XLM", - Issuer: "", - }, + assets.USDCAssetTestnet, + assets.XLMAsset, }, }, { @@ -25,10 +22,7 @@ var TestnetWallets = []data.Wallet{ DeepLinkSchema: "https://vibrantapp.com/sdp-dev", SEP10ClientDomain: "api-dev.vibrantapp.com", Assets: []data.Asset{ - { - Code: "USDC", - Issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - }, + assets.USDCAssetTestnet, }, }, }