From 5a7e1cdc9b6aa17681595eaf664d2fdad67aee4d Mon Sep 17 00:00:00 2001 From: Erica Liu Date: Fri, 15 Dec 2023 16:16:06 -0800 Subject: [PATCH] [SDP-962] Change SEP-24 Flow to display different verifications based on Disbursement's verification type (#116) Modify the SEP-24 flow to perform verification for an entered phone number based on the latest verification type. The current SEP-24 flow is hardcoded to only accept date of birth but we will have disbursement files that will include pin and national id, and the front-end will need to change to be able to parse those values. Release 1.0.1 to develop (#129) [Release 1.0.1](https://github.com/stellar/stellar-disbursement-platform-backend/issues/127) to develop To sync the `main` branch hotfixes: - #125 - #126 changes: fix tests migrate --- CHANGELOG.md | 10 ++++ cmd/db_test.go | 4 +- helmchart/sdp/Chart.yaml | 2 +- internal/data/disbursement_instructions.go | 20 ++++--- .../data/disbursement_instructions_test.go | 29 ++++++++++ internal/data/disbursements_test.go | 6 +- internal/data/fixtures_test.go | 10 ++-- internal/data/models.go | 2 +- internal/data/payments.go | 22 ++++--- internal/data/receiver_verification.go | 44 +++++++++++--- internal/data/receiver_verification_test.go | 58 +++++++++++++++++++ ...payments-table-add-external-payment-id.sql | 9 +++ .../htmltemplate/tmpl/receiver_register.tmpl | 7 +-- .../httphandler/payments_handler_test.go | 2 +- .../httphandler/receiver_send_otp_handler.go | 19 ++++-- .../receiver_send_otp_handler_test.go | 53 +++++++++++++++-- .../publicfiles/js/receiver_registration.js | 26 +++++++-- internal/serve/serve.go | 5 +- .../receiver_registration_validator.go | 13 ++++- .../receiver_registration_validator_test.go | 46 +++++++++++++++ .../setup_wallets_for_network_service_test.go | 12 ++-- internal/services/wallets/wallets_pubnet.go | 2 +- .../transactionsubmission/utils/errors.go | 2 +- main.go | 2 +- 24 files changed, 339 insertions(+), 66 deletions(-) create mode 100644 internal/db/migrations/2023-12-18.0-alter-payments-table-add-external-payment-id.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b28eccd..b50266df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add sorting to `GET /users` endpoint [#104](https://github.com/stellar/stellar-disbursement-platform-backend/pull/104) +## [1.0.1](https://github.com/stellar/stellar-disbursement-platform-backend/compare/1.0.0...1.0.1) + +### Changed + +- Update log message for better debugging. [#125](https://github.com/stellar/stellar-disbursement-platform-backend/pull/125) + +### Fixed + +- Fix client_domain from the Viobrant Assist wallet. [#126](https://github.com/stellar/stellar-disbursement-platform-backend/pull/126) + ## [1.0.0](https://github.com/stellar/stellar-disbursement-platform-backend/compare/1.0.0-rc2...1.0.0) ### Added diff --git a/cmd/db_test.go b/cmd/db_test.go index 4a4855660..4d0195e6d 100644 --- a/cmd/db_test.go +++ b/cmd/db_test.go @@ -226,7 +226,7 @@ func Test_DatabaseCommand_db_setup_for_network(t *testing.T) { // Test the two wallets assert.Equal(t, "Vibrant Assist", vibrantAssist.Name) assert.Equal(t, "https://vibrantapp.com/vibrant-assist", vibrantAssist.Homepage) - assert.Equal(t, "api.vibrantapp.com", vibrantAssist.SEP10ClientDomain) + assert.Equal(t, "vibrantapp.com", vibrantAssist.SEP10ClientDomain) assert.Equal(t, "https://vibrantapp.com/sdp", vibrantAssist.DeepLinkSchema) assert.Equal(t, "Vibrant Assist RC", vibrantAssistRC.Name) @@ -244,7 +244,7 @@ func Test_DatabaseCommand_db_setup_for_network(t *testing.T) { "Name: Vibrant Assist", "Homepage: https://vibrantapp.com/vibrant-assist", "Deep Link Schema: https://vibrantapp.com/sdp", - "SEP-10 Client Domain: api.vibrantapp.com", + "SEP-10 Client Domain: vibrantapp.com", } logs := buf.String() diff --git a/helmchart/sdp/Chart.yaml b/helmchart/sdp/Chart.yaml index 30eed34ec..4fb5d9ffe 100644 --- a/helmchart/sdp/Chart.yaml +++ b/helmchart/sdp/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: stellar-disbursement-platform description: A Helm chart for the Stellar Disbursement Platform Backend (A.K.A. `sdp`) version: 0.9.3 -appVersion: "1.0.0" +appVersion: "1.0.1" type: application maintainers: - name: Stellar Development Foundation diff --git a/internal/data/disbursement_instructions.go b/internal/data/disbursement_instructions.go index 1fd794772..775c9e09b 100644 --- a/internal/data/disbursement_instructions.go +++ b/internal/data/disbursement_instructions.go @@ -9,10 +9,11 @@ import ( ) type DisbursementInstruction struct { - Phone string `csv:"phone"` - ID string `csv:"id"` - Amount string `csv:"amount"` - VerificationValue string `csv:"verification"` + Phone string `csv:"phone"` + ID string `csv:"id"` + Amount string `csv:"amount"` + VerificationValue string `csv:"verification"` + ExternalPaymentId *string `csv:"paymentID"` } type DisbursementInstructionModel struct { @@ -194,11 +195,12 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID st for _, instruction := range instructions { receiver := receiverMap[instruction.Phone] payment := PaymentInsert{ - ReceiverID: receiver.ID, - DisbursementID: disbursement.ID, - Amount: instruction.Amount, - AssetID: disbursement.Asset.ID, - ReceiverWalletID: receiverWalletsMap[receiver.ID], + ReceiverID: receiver.ID, + DisbursementID: disbursement.ID, + Amount: instruction.Amount, + AssetID: disbursement.Asset.ID, + ReceiverWalletID: receiverWalletsMap[receiver.ID], + ExternalPaymentID: instruction.ExternalPaymentId, } payments = append(payments, payment) } diff --git a/internal/data/disbursement_instructions_test.go b/internal/data/disbursement_instructions_test.go index 21ab1d2e8..fa04b253b 100644 --- a/internal/data/disbursement_instructions_test.go +++ b/internal/data/disbursement_instructions_test.go @@ -2,6 +2,7 @@ package data import ( "context" + "database/sql" "testing" "github.com/stellar/stellar-disbursement-platform-backend/internal/db" @@ -47,16 +48,19 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { VerificationValue: "1990-01-02", } + externalPaymentID := "abc123" instruction3 := DisbursementInstruction{ Phone: "+380-12-345-673", Amount: "100.03", ID: "123456783", VerificationValue: "1990-01-03", + ExternalPaymentId: &externalPaymentID, } instructions := []*DisbursementInstruction{&instruction1, &instruction2, &instruction3} expectedPhoneNumbers := []string{instruction1.Phone, instruction2.Phone, instruction3.Phone} expectedExternalIDs := []string{instruction1.ID, instruction2.ID, instruction3.ID} expectedPayments := []string{instruction1.Amount, instruction2.Amount, instruction3.Amount} + expectedExternalPaymentIDs := []string{*instruction3.ExternalPaymentId} disbursementUpdate := &DisbursementUpdate{ ID: disbursement.ID, @@ -91,6 +95,9 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { actualPayments := GetPaymentsByDisbursementID(t, ctx, dbConnectionPool, disbursement.ID) assert.Equal(t, expectedPayments, actualPayments) + actualExternalPaymentIDs := GetExternalPaymentIDsByDisbursementID(t, ctx, dbConnectionPool, disbursement.ID) + assert.Equal(t, expectedExternalPaymentIDs, actualExternalPaymentIDs) + // Verify Disbursement actualDisbursement, err := di.disbursementModel.Get(ctx, dbConnectionPool, disbursement.ID) require.NoError(t, err) @@ -304,3 +311,25 @@ func GetPaymentsByDisbursementID(t *testing.T, ctx context.Context, dbConnection require.NoError(t, err) return payments } + +func GetExternalPaymentIDsByDisbursementID(t *testing.T, ctx context.Context, dbConnectionPool db.DBConnectionPool, disbursementID string) []string { + query := ` + SELECT + p.external_payment_id + FROM + payments p + WHERE p.disbursement_id = $1 + ` + var externalPaymentIDRefs []sql.NullString + err := dbConnectionPool.SelectContext(ctx, &externalPaymentIDRefs, query, disbursementID) + require.NoError(t, err) + + var externalPaymentIDs []string + for _, v := range externalPaymentIDRefs { + if v.String != "" { + externalPaymentIDs = append(externalPaymentIDs, v.String) + } + } + + return externalPaymentIDs +} diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go index ec3d06e3a..fdb2cb47f 100644 --- a/internal/data/disbursements_test.go +++ b/internal/data/disbursements_test.go @@ -437,9 +437,9 @@ func Test_DisbursementModel_Update(t *testing.T) { }) disbursementFileContent := CreateInstructionsFixture(t, []*DisbursementInstruction{ - {"1234567890", "1", "123.12", "1995-02-20"}, - {"0987654321", "2", "321", "1974-07-19"}, - {"0987654321", "3", "321", "1974-07-19"}, + {"1234567890", "1", "123.12", "1995-02-20", nil}, + {"0987654321", "2", "321", "1974-07-19", nil}, + {"0987654321", "3", "321", "1974-07-19", nil}, }) t.Run("update instructions", func(t *testing.T) { diff --git a/internal/data/fixtures_test.go b/internal/data/fixtures_test.go index f48136f00..e06e952a4 100644 --- a/internal/data/fixtures_test.go +++ b/internal/data/fixtures_test.go @@ -89,8 +89,8 @@ func Test_Fixtures_CreateInstructionsFixture(t *testing.T) { t.Run("writes records correctly", func(t *testing.T) { instructions := []*DisbursementInstruction{ - {"1234567890", "1", "123.12", "1995-02-20"}, - {"0987654321", "2", "321", "1974-07-19"}, + {"1234567890", "1", "123.12", "1995-02-20", nil}, + {"0987654321", "2", "321", "1974-07-19", nil}, } buf := CreateInstructionsFixture(t, instructions) lines := strings.Split(string(buf), "\n") @@ -116,9 +116,9 @@ func Test_Fixtures_UpdateDisbursementInstructionsFixture(t *testing.T) { }) instructions := []*DisbursementInstruction{ - {"1234567890", "1", "123.12", "1995-02-20"}, - {"0987654321", "2", "321", "1974-07-19"}, - {"0987654321", "3", "321", "1974-07-19"}, + {"1234567890", "1", "123.12", "1995-02-20", nil}, + {"0987654321", "2", "321", "1974-07-19", nil}, + {"0987654321", "3", "321", "1974-07-19", nil}, } t.Run("update instructions", func(t *testing.T) { diff --git a/internal/data/models.go b/internal/data/models.go index e8946829c..83d83be11 100644 --- a/internal/data/models.go +++ b/internal/data/models.go @@ -42,7 +42,7 @@ func NewModels(dbConnectionPool db.DBConnectionPool) (*Models, error) { Payment: &PaymentModel{dbConnectionPool: dbConnectionPool}, Receiver: &ReceiverModel{}, DisbursementInstructions: NewDisbursementInstructionModel(dbConnectionPool), - ReceiverVerification: &ReceiverVerificationModel{}, + ReceiverVerification: &ReceiverVerificationModel{dbConnectionPool: dbConnectionPool}, ReceiverWallet: &ReceiverWalletModel{dbConnectionPool: dbConnectionPool}, DisbursementReceivers: &DisbursementReceiverModel{dbConnectionPool: dbConnectionPool}, Message: &MessageModel{dbConnectionPool: dbConnectionPool}, diff --git a/internal/data/payments.go b/internal/data/payments.go index d4468ccfc..475224e55 100644 --- a/internal/data/payments.go +++ b/internal/data/payments.go @@ -30,6 +30,7 @@ type Payment struct { ReceiverWallet *ReceiverWallet `json:"receiver_wallet,omitempty" db:"receiver_wallet"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + ExternalPaymentID *string `json:"external_payment_id,omitempty" db:"external_payment_id"` } type PaymentStatusHistoryEntry struct { @@ -50,11 +51,12 @@ var ( ) type PaymentInsert struct { - ReceiverID string `db:"receiver_id"` - DisbursementID string `db:"disbursement_id"` - Amount string `db:"amount"` - AssetID string `db:"asset_id"` - ReceiverWalletID string `db:"receiver_wallet_id"` + ReceiverID string `db:"receiver_id"` + DisbursementID string `db:"disbursement_id"` + Amount string `db:"amount"` + AssetID string `db:"asset_id"` + ReceiverWalletID string `db:"receiver_wallet_id"` + ExternalPaymentID *string `db:"external_payment_id"` } type PaymentUpdate struct { @@ -150,6 +152,7 @@ func (p *PaymentModel) Get(ctx context.Context, id string, sqlExec db.SQLExecute p.status_history, p.created_at, p.updated_at, + p.external_payment_id, d.id as "disbursement.id", d.name as "disbursement.name", d.status as "disbursement.status", @@ -227,6 +230,7 @@ func (p *PaymentModel) GetAll(ctx context.Context, queryParams *QueryParams, sql p.status_history, p.created_at, p.updated_at, + p.external_payment_id, d.id as "disbursement.id", d.name as "disbursement.name", d.status as "disbursement.status", @@ -341,18 +345,20 @@ func (p *PaymentModel) InsertAll(ctx context.Context, sqlExec db.SQLExecuter, in asset_id, receiver_id, disbursement_id, - receiver_wallet_id + receiver_wallet_id, + external_payment_id ) VALUES ( $1, $2, $3, $4, - $5 + $5, + $6 ) ` for _, payment := range inserts { - _, err := sqlExec.ExecContext(ctx, query, payment.Amount, payment.AssetID, payment.ReceiverID, payment.DisbursementID, payment.ReceiverWalletID) + _, err := sqlExec.ExecContext(ctx, query, payment.Amount, payment.AssetID, payment.ReceiverID, payment.DisbursementID, payment.ReceiverWalletID, payment.ExternalPaymentID) if err != nil { return fmt.Errorf("error inserting payment: %w", err) } diff --git a/internal/data/receiver_verification.go b/internal/data/receiver_verification.go index d85a25502..f660cca32 100644 --- a/internal/data/receiver_verification.go +++ b/internal/data/receiver_verification.go @@ -2,6 +2,8 @@ package data import ( "context" + "database/sql" + "errors" "fmt" "strings" "time" @@ -24,7 +26,9 @@ type ReceiverVerification struct { FailedAt *time.Time `db:"failed_at"` } -type ReceiverVerificationModel struct{} +type ReceiverVerificationModel struct { + dbConnectionPool db.DBConnectionPool +} type ReceiverVerificationInsert struct { ReceiverID string `db:"receiver_id"` @@ -48,7 +52,7 @@ func (rvi *ReceiverVerificationInsert) Validate() error { } // GetByReceiverIDsAndVerificationField returns receiver verifications by receiver IDs and verification type. -func (m ReceiverVerificationModel) GetByReceiverIDsAndVerificationField(ctx context.Context, sqlExec db.SQLExecuter, receiverIds []string, verificationField VerificationField) ([]*ReceiverVerification, error) { +func (m *ReceiverVerificationModel) GetByReceiverIDsAndVerificationField(ctx context.Context, sqlExec db.SQLExecuter, receiverIds []string, verificationField VerificationField) ([]*ReceiverVerification, error) { receiverVerifications := []*ReceiverVerification{} query := ` SELECT @@ -74,7 +78,7 @@ func (m ReceiverVerificationModel) GetByReceiverIDsAndVerificationField(ctx cont } // GetAllByReceiverId returns all receiver verifications by receiver id. -func (m ReceiverVerificationModel) GetAllByReceiverId(ctx context.Context, sqlExec db.SQLExecuter, receiverId string) ([]ReceiverVerification, error) { +func (m *ReceiverVerificationModel) GetAllByReceiverId(ctx context.Context, sqlExec db.SQLExecuter, receiverId string) ([]ReceiverVerification, error) { receiverVerifications := []ReceiverVerification{} query := ` SELECT @@ -91,8 +95,35 @@ func (m ReceiverVerificationModel) GetAllByReceiverId(ctx context.Context, sqlEx return receiverVerifications, nil } +// GetLatestByPhoneNumber returns the latest updated receiver verification for some receiver that is associated with a phone number. +func (m *ReceiverVerificationModel) GetLatestByPhoneNumber(ctx context.Context, phoneNumber string) (*ReceiverVerification, error) { + receiverVerification := ReceiverVerification{} + query := ` + SELECT + rv.* + FROM + receiver_verifications rv + JOIN receivers r ON rv.receiver_id = r.id + WHERE + r.phone_number = $1 + ORDER BY + rv.updated_at DESC + LIMIT 1 + ` + + err := m.dbConnectionPool.GetContext(ctx, &receiverVerification, query, phoneNumber) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrRecordNotFound + } + return nil, fmt.Errorf("fetching receiver verifications for phone number %s: %w", phoneNumber, err) + } + + return &receiverVerification, nil +} + // Insert inserts a new receiver verification -func (m ReceiverVerificationModel) Insert(ctx context.Context, sqlExec db.SQLExecuter, verificationInsert ReceiverVerificationInsert) (string, error) { +func (m *ReceiverVerificationModel) Insert(ctx context.Context, sqlExec db.SQLExecuter, verificationInsert ReceiverVerificationInsert) (string, error) { err := verificationInsert.Validate() if err != nil { return "", fmt.Errorf("error validating receiver verification insert: %w", err) @@ -111,7 +142,6 @@ func (m ReceiverVerificationModel) Insert(ctx context.Context, sqlExec db.SQLExe ` _, err = sqlExec.ExecContext(ctx, query, verificationInsert.ReceiverID, verificationInsert.VerificationField, hashedValue) - if err != nil { return "", fmt.Errorf("error inserting receiver verification: %w", err) } @@ -120,7 +150,7 @@ func (m ReceiverVerificationModel) Insert(ctx context.Context, sqlExec db.SQLExe } // UpdateVerificationValue updates the hashed value of a receiver verification. -func (m ReceiverVerificationModel) UpdateVerificationValue(ctx context.Context, +func (m *ReceiverVerificationModel) UpdateVerificationValue(ctx context.Context, sqlExec db.SQLExecuter, receiverID string, verificationField VerificationField, @@ -148,7 +178,7 @@ func (m ReceiverVerificationModel) UpdateVerificationValue(ctx context.Context, } // UpdateVerificationValue updates the hashed value of a receiver verification. -func (m ReceiverVerificationModel) UpdateReceiverVerification(ctx context.Context, receiverVerification ReceiverVerification, sqlExec db.SQLExecuter) error { +func (m *ReceiverVerificationModel) UpdateReceiverVerification(ctx context.Context, receiverVerification ReceiverVerification, sqlExec db.SQLExecuter) error { query := ` UPDATE receiver_verifications diff --git a/internal/data/receiver_verification_test.go b/internal/data/receiver_verification_test.go index 9e1238c51..c68b8a636 100644 --- a/internal/data/receiver_verification_test.go +++ b/internal/data/receiver_verification_test.go @@ -2,6 +2,7 @@ package data import ( "context" + "fmt" "testing" "time" @@ -125,6 +126,63 @@ func Test_ReceiverVerificationModel_GetAllByReceiverId(t *testing.T) { }) } +func Test_ReceiverVerificationModel_GetReceiverVerificationByReceiverId(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + + ctx := context.Background() + + receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{ + PhoneNumber: "+13334445555", + }) + + t.Run("returns error when the receiver has no verifications registered", func(t *testing.T) { + receiverVerificationModel := ReceiverVerificationModel{dbConnectionPool: dbConnectionPool} + _, err := receiverVerificationModel.GetLatestByPhoneNumber(ctx, receiver.PhoneNumber) + require.Error(t, err, fmt.Errorf("cannot query any receiver verifications for phone number %s", receiver.PhoneNumber)) + }) + + t.Run("returns the latest receiver verification for a list of receiver verifications", func(t *testing.T) { + earlierTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + verification1 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ + ReceiverID: receiver.ID, + VerificationField: VerificationFieldDateOfBirth, + VerificationValue: "1990-01-01", + }) + verification1.UpdatedAt = earlierTime + + verification2 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ + ReceiverID: receiver.ID, + VerificationField: VerificationFieldPin, + VerificationValue: "1234", + }) + verification2.UpdatedAt = earlierTime + + verification3 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ + ReceiverID: receiver.ID, + VerificationField: VerificationFieldNationalID, + VerificationValue: "5678", + }) + + receiverVerificationModel := ReceiverVerificationModel{dbConnectionPool: dbConnectionPool} + actualVerification, err := receiverVerificationModel.GetLatestByPhoneNumber(ctx, receiver.PhoneNumber) + require.NoError(t, err) + + assert.Equal(t, + ReceiverVerification{ + ReceiverID: receiver.ID, + VerificationField: VerificationFieldNationalID, + HashedValue: verification3.HashedValue, + CreatedAt: verification3.CreatedAt, + UpdatedAt: verification3.UpdatedAt, + }, *actualVerification) + }) +} + func Test_ReceiverVerificationModel_Insert(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() diff --git a/internal/db/migrations/2023-12-18.0-alter-payments-table-add-external-payment-id.sql b/internal/db/migrations/2023-12-18.0-alter-payments-table-add-external-payment-id.sql new file mode 100644 index 000000000..a480d7c1d --- /dev/null +++ b/internal/db/migrations/2023-12-18.0-alter-payments-table-add-external-payment-id.sql @@ -0,0 +1,9 @@ +-- +migrate Up + +ALTER TABLE public.payments + ADD COLUMN external_payment_id VARCHAR(64) NULL; + +-- +migrate Down + +ALTER TABLE public.payments + DROP COLUMN external_payment_id; diff --git a/internal/htmltemplate/tmpl/receiver_register.tmpl b/internal/htmltemplate/tmpl/receiver_register.tmpl index d74b02781..f06ae3772 100644 --- a/internal/htmltemplate/tmpl/receiver_register.tmpl +++ b/internal/htmltemplate/tmpl/receiver_register.tmpl @@ -87,7 +87,7 @@ - +

Enter passcode

@@ -119,14 +119,11 @@ />
- + -
diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go index 617733fe5..b91a40d51 100644 --- a/internal/serve/httphandler/payments_handler_test.go +++ b/internal/serve/httphandler/payments_handler_test.go @@ -139,7 +139,7 @@ func Test_PaymentsHandlerGet(t *testing.T) { "invitation_sent_at": null }, "created_at": %q, - "updated_at": %q + "updated_at": %q }`, payment.ID, payment.StellarTransactionID, payment.StellarOperationID, payment.StatusHistory[0].Timestamp.Format(time.RFC3339Nano), disbursement.ID, disbursement.CreatedAt.Format(time.RFC3339Nano), disbursement.UpdatedAt.Format(time.RFC3339Nano), asset.ID, receiverWallet.ID, receiver.ID, wallet.ID, receiverWallet.StellarAddress, receiverWallet.StellarMemo, diff --git a/internal/serve/httphandler/receiver_send_otp_handler.go b/internal/serve/httphandler/receiver_send_otp_handler.go index cc76b74e7..120136412 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler.go +++ b/internal/serve/httphandler/receiver_send_otp_handler.go @@ -35,7 +35,8 @@ type ReceiverSendOTPRequest struct { } type ReceiverSendOTPResponseBody struct { - Message string `json:"message"` + Message string `json:"message"` + VerificationField data.VerificationField `json:"verification_field"` } func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -62,10 +63,11 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } + truncatedPhoneNumber := utils.TruncateString(receiverSendOTPRequest.PhoneNumber, 3) if phoneValidateErr := utils.ValidatePhoneNumber(receiverSendOTPRequest.PhoneNumber); phoneValidateErr != nil { extras := map[string]interface{}{"phone_number": "phone_number is required"} if !errors.Is(phoneValidateErr, utils.ErrEmptyPhoneNumber) { - phoneValidateErr = fmt.Errorf("validating phone number %s: %w", utils.TruncateString(receiverSendOTPRequest.PhoneNumber, len(receiverSendOTPRequest.PhoneNumber)/4), phoneValidateErr) + phoneValidateErr = fmt.Errorf("validating phone number %s: %w", truncatedPhoneNumber, phoneValidateErr) log.Ctx(ctx).Error(phoneValidateErr) extras["phone_number"] = "invalid phone number provided" } @@ -90,6 +92,12 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } + receiverVerification, err := h.Models.ReceiverVerification.GetLatestByPhoneNumber(ctx, receiverSendOTPRequest.PhoneNumber) + if err != nil { + httperror.InternalError(ctx, "Cannot find latest receiver verification for receiver", err, nil).Render(w) + return + } + // Generate a new 6 digits OTP newOTP, err := utils.RandomString(6, utils.NumberBytes) if err != nil { @@ -110,7 +118,7 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request } if numberOfUpdatedRows < 1 { - log.Ctx(ctx).Warnf("updated no rows in receiver send OTP handler for phone number: %s", utils.TruncateString(receiverSendOTPRequest.PhoneNumber, len(receiverSendOTPRequest.PhoneNumber)/4)) + log.Ctx(ctx).Warnf("updated no rows in ReceiverSendOTPHandler, please verify if the provided phone number (%s) and client_domain (%s) are both valid", truncatedPhoneNumber, sep24Claims.ClientDomainClaim) } else { sendOTPData := ReceiverSendOTPData{ OTP: newOTP, @@ -140,7 +148,7 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request Message: builder.String(), } - log.Ctx(ctx).Infof("sending OTP message to phone number: %s", utils.TruncateString(receiverSendOTPRequest.PhoneNumber, 3)) + log.Ctx(ctx).Infof("sending OTP message to phone number: %s", truncatedPhoneNumber) err = h.SMSMessengerClient.SendMessage(smsMessage) if err != nil { httperror.InternalError(ctx, "Cannot send OTP message", err, nil).Render(w) @@ -149,7 +157,8 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request } response := ReceiverSendOTPResponseBody{ - Message: "if your phone number is registered, you'll receive an OTP", + Message: "if your phone number is registered, you'll receive an OTP", + VerificationField: receiverVerification.VerificationField, } httpjson.RenderStatus(w, http.StatusOK, response, httpjson.JSON) } diff --git a/internal/serve/httphandler/receiver_send_otp_handler_test.go b/internal/serve/httphandler/receiver_send_otp_handler_test.go index 6be199eee..edd44d468 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler_test.go +++ b/internal/serve/httphandler/receiver_send_otp_handler_test.go @@ -53,9 +53,14 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { ctx := context.Background() - receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: "+380443973607"}) + phoneNumber := "+380443973607" + receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{}) wallet1 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://home.page", "home.page", "wallet123://") + data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ + ReceiverID: receiver1.ID, + VerificationField: data.VerificationFieldDateOfBirth, + }) _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.RegisteredReceiversWalletStatus) _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet1.ID, data.RegisteredReceiversWalletStatus) @@ -157,7 +162,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { assert.JSONEq(t, `{"error": "request invalid", "extras": {"phone_number": "invalid phone number provided"}}`, string(respBody)) }) - t.Run("returns 200 - Ok if the token is in the request context and body it's valid", func(t *testing.T) { + t.Run("returns 200 - Ok if the token is in the request context and body is valid", func(t *testing.T) { reCAPTCHAValidator. On("IsTokenValid", mock.Anything, "XyZ"). Return(true, nil). @@ -193,7 +198,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, resp.Header.Get("Content-Type"), "/json; charset=utf-8") - assert.JSONEq(t, string(respBody), `{"message":"if your phone number is registered, you'll receive an OTP"}`) + assert.JSONEq(t, string(respBody), `{"message":"if your phone number is registered, you'll receive an OTP", "verification_field":"DATE_OF_BIRTH"}`) }) t.Run("returns 200 - parses a custom OTP message template successfully", func(t *testing.T) { @@ -237,7 +242,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Contains(t, resp.Header.Get("Content-Type"), "/json; charset=utf-8") - assert.JSONEq(t, string(respBody), `{"message":"if your phone number is registered, you'll receive an OTP"}`) + assert.JSONEq(t, string(respBody), `{"message":"if your phone number is registered, you'll receive an OTP", "verification_field":"DATE_OF_BIRTH"}`) }) t.Run("returns 500 - InternalServerError when something goes wrong when sending the SMS", func(t *testing.T) { @@ -309,6 +314,46 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { assert.JSONEq(t, wantsBody, string(respBody)) }) + t.Run("returns 500 - InternalServerError if phone number is not associated with receiver verification", func(t *testing.T) { + requestSendOTP := ReceiverSendOTPRequest{ + PhoneNumber: "+14152223333", + ReCAPTCHAToken: "XyZ", + } + reqBody, _ = json.Marshal(requestSendOTP) + + reCAPTCHAValidator. + On("IsTokenValid", mock.Anything, "XyZ"). + Return(true, nil). + Once() + req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) + require.NoError(t, err) + + validClaims := &anchorplatform.SEP24JWTClaims{ + ClientDomainClaim: wallet1.SEP10ClientDomain, + RegisteredClaims: jwt.RegisteredClaims{ + ID: "test-transaction-id", + Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + }, + } + req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + resp := rr.Result() + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + wantsBody := ` + { + "error": "Cannot find latest receiver verification for receiver" + } + ` + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + assert.JSONEq(t, wantsBody, string(respBody)) + }) + t.Run("returns 400 - BadRequest when recaptcha token is invalid", func(t *testing.T) { reCAPTCHAValidator. On("IsTokenValid", mock.Anything, "XyZ"). diff --git a/internal/serve/publicfiles/js/receiver_registration.js b/internal/serve/publicfiles/js/receiver_registration.js index 2065d3572..2ff2ae57c 100644 --- a/internal/serve/publicfiles/js/receiver_registration.js +++ b/internal/serve/publicfiles/js/receiver_registration.js @@ -51,9 +51,9 @@ async function sendSms(phoneNumber, reCAPTCHAToken, onSuccess, onError) { recaptcha_token: reCAPTCHAToken, }), }); - await request.json(); + const resp = await request.json(); - onSuccess(); + onSuccess(resp.verification_field); } catch (error) { onError(error); } @@ -96,6 +96,8 @@ async function submitPhoneNumber(event) { "#g-recaptcha-response" ); const buttonEls = phoneNumberSectionEl.querySelectorAll("[data-button]"); + const verificationFieldTitle = document.querySelector("label[for='verification']"); + const verificationFieldInput = document.querySelector("#verification"); if (!reCAPTCHATokenEl || !reCAPTCHATokenEl.value) { toggleErrorNotification( @@ -133,7 +135,22 @@ async function submitPhoneNumber(event) { return; } - function showNextPage() { + function showNextPage(verificationField) { + verificationFieldInput.type = "text"; + if(verificationField === "DATE_OF_BIRTH") { + verificationFieldTitle.textContent = "Date of birth"; + verificationFieldInput.name = "date_of_birth"; + verificationFieldInput.type = "date"; + } + else if(verificationField === "NATIONAL_ID_NUMBER") { + verificationFieldTitle.textContent = "National ID number"; + verificationFieldInput.name = "national_id_number"; + } + else if(verificationField === "PIN") { + verificationFieldTitle.textContent = "Pin"; + verificationFieldInput.name = "pin"; + } + phoneNumberSectionEl.style.display = "none"; reCAPTCHATokenEl.style.display = "none"; passcodeSectionEl.style.display = "flex"; @@ -169,6 +186,7 @@ async function submitOtp(event) { ); const otpEl = document.getElementById("otp"); const verificationEl = document.getElementById("verification"); + const verificationField = verificationEl.getAttribute("name"); const buttonEls = passcodeSectionEl.querySelectorAll("[data-button]"); @@ -213,7 +231,7 @@ async function submitOtp(event) { phone_number: phoneNumber, otp: otp, verification: verification, - verification_type: "date_of_birth", + verification_type: verificationField, recaptcha_token: reCAPTCHATokenEl.value, }), }); diff --git a/internal/serve/serve.go b/internal/serve/serve.go index e97c1384f..9d58dcd07 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -380,7 +380,10 @@ func handleHTTP(o ServeOptions) *chi.Mux { mux.Route("/wallet-registration", func(r chi.Router) { sep24QueryTokenAuthenticationMiddleware := anchorplatform.SEP24QueryTokenAuthenticateMiddleware(o.sep24JWTManager, o.NetworkPassphrase) - r.With(sep24QueryTokenAuthenticationMiddleware).Get("/start", httphandler.ReceiverRegistrationHandler{ReceiverWalletModel: o.Models.ReceiverWallet, ReCAPTCHASiteKey: o.ReCAPTCHASiteKey}.ServeHTTP) // This loads the SEP-24 PII registration webpage. + r.With(sep24QueryTokenAuthenticationMiddleware).Get("/start", httphandler.ReceiverRegistrationHandler{ + ReceiverWalletModel: o.Models.ReceiverWallet, + ReCAPTCHASiteKey: o.ReCAPTCHASiteKey, + }.ServeHTTP) // This loads the SEP-24 PII registration webpage. sep24HeaderTokenAuthenticationMiddleware := anchorplatform.SEP24HeaderTokenAuthenticateMiddleware(o.sep24JWTManager, o.NetworkPassphrase) r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/otp", httphandler.ReceiverSendOTPHandler{Models: o.Models, SMSMessengerClient: o.SMSMessengerClient, ReCAPTCHAValidator: reCAPTCHAValidator}.ServeHTTP) diff --git a/internal/serve/validators/receiver_registration_validator.go b/internal/serve/validators/receiver_registration_validator.go index b7b2b5e94..e0a57904c 100644 --- a/internal/serve/validators/receiver_registration_validator.go +++ b/internal/serve/validators/receiver_registration_validator.go @@ -42,8 +42,19 @@ func (rv *ReceiverRegistrationValidator) ValidateReceiver(receiverInfo *data.Rec // validate verification field // date of birth with format 2006-01-02 if vt == data.VerificationFieldDateOfBirth { - _, err := time.Parse("2006-01-02", verification) + dob, err := time.Parse("2006-01-02", verification) rv.CheckError(err, "verification", "invalid date of birth format. Correct format: 1990-01-01") + + // check if date of birth is in the past + rv.Check(dob.Before(time.Now()), "verification", "date of birth cannot be in the future") + } else if vt == data.VerificationFieldPin { + if len(verification) < VERIFICATION_FIELD_PIN_MIN_LENGTH || len(verification) > VERIFICATION_FIELD_PIN_MAX_LENGTH { + rv.addError("verification", "invalid pin. Cannot have less than 4 or more than 8 characters in pin") + } + } else if vt == data.VerificationFieldNationalID { + if len(verification) > VERIFICATION_FIELD_MAX_ID_LENGTH { + rv.addError("verification", "invalid national id. Cannot have more than 50 characters in national id") + } } else { // TODO: validate other VerificationField types. log.Warnf("Verification type %v is not being validated for ValidateReceiver", vt) diff --git a/internal/serve/validators/receiver_registration_validator_test.go b/internal/serve/validators/receiver_registration_validator_test.go index 2cb6257d3..cebce9c71 100644 --- a/internal/serve/validators/receiver_registration_validator_test.go +++ b/internal/serve/validators/receiver_registration_validator_test.go @@ -83,6 +83,36 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { assert.Equal(t, "invalid date of birth format. Correct format: 1990-01-01", validator.Errors["verification"]) }) + t.Run("Invalid pin", func(t *testing.T) { + validator := NewReceiverRegistrationValidator() + + receiverInfo := data.ReceiverRegistrationRequest{ + PhoneNumber: "+380445555555", + OTP: "123456", + VerificationValue: "ABCDE1234", + VerificationType: "PIN", + } + validator.ValidateReceiver(&receiverInfo) + + assert.Equal(t, 1, len(validator.Errors)) + assert.Equal(t, "invalid pin. Cannot have less than 4 or more than 8 characters in pin", validator.Errors["verification"]) + }) + + t.Run("Invalid national ID number", func(t *testing.T) { + validator := NewReceiverRegistrationValidator() + + receiverInfo := data.ReceiverRegistrationRequest{ + PhoneNumber: "+380445555555", + OTP: "123456", + VerificationValue: "6UZMB56FWTKV4U0PJ21TBR6VOQVYSGIMZG2HW2S0L7EK5K83W78XXXXX", + VerificationType: "NATIONAL_ID_NUMBER", + } + validator.ValidateReceiver(&receiverInfo) + + assert.Equal(t, 1, len(validator.Errors)) + assert.Equal(t, "invalid national id. Cannot have more than 50 characters in national id", validator.Errors["verification"]) + }) + t.Run("Valid receiver values", func(t *testing.T) { validator := NewReceiverRegistrationValidator() @@ -99,6 +129,22 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { assert.Equal(t, "123456", receiverInfo.OTP) assert.Equal(t, "1990-01-01", receiverInfo.VerificationValue) assert.Equal(t, data.VerificationField("DATE_OF_BIRTH"), receiverInfo.VerificationType) + + receiverInfo.VerificationValue = "1234" + receiverInfo.VerificationType = "pin" + validator.ValidateReceiver(&receiverInfo) + + assert.Equal(t, 0, len(validator.Errors)) + assert.Equal(t, "1234", receiverInfo.VerificationValue) + assert.Equal(t, data.VerificationField("PIN"), receiverInfo.VerificationType) + + receiverInfo.VerificationValue = "NATIONALIDNUMBER123" + receiverInfo.VerificationType = "national_id_number" + validator.ValidateReceiver(&receiverInfo) + + assert.Equal(t, 0, len(validator.Errors)) + assert.Equal(t, "NATIONALIDNUMBER123", receiverInfo.VerificationValue) + assert.Equal(t, data.VerificationField("NATIONAL_ID_NUMBER"), receiverInfo.VerificationType) }) } diff --git a/internal/services/setup_wallets_for_network_service_test.go b/internal/services/setup_wallets_for_network_service_test.go index 6a6200b69..f476bfbd4 100644 --- a/internal/services/setup_wallets_for_network_service_test.go +++ b/internal/services/setup_wallets_for_network_service_test.go @@ -72,7 +72,7 @@ func Test_SetupWalletsForProperNetwork(t *testing.T) { "Name: Vibrant Assist", "Homepage: https://vibrantapp.com/vibrant-assist", "Deep Link Schema: https://vibrantapp.com/sdp", - "SEP-10 Client Domain: api.vibrantapp.com", + "SEP-10 Client Domain: vibrantapp.com", } logs := buf.String() @@ -101,7 +101,7 @@ func Test_SetupWalletsForProperNetwork(t *testing.T) { Name: "Vibrant Assist", Homepage: "https://vibrantapp.com/vibrant-assist", DeepLinkSchema: "https://aidpubnet.netlify.app", - SEP10ClientDomain: "api.vibrantapp.com", + SEP10ClientDomain: "vibrantapp.com", }, { Name: "BOSS Money", @@ -130,7 +130,7 @@ func Test_SetupWalletsForProperNetwork(t *testing.T) { assert.Equal(t, "Vibrant Assist", wallets[1].Name) assert.Equal(t, "https://vibrantapp.com/vibrant-assist", wallets[1].Homepage) - assert.Equal(t, "api.vibrantapp.com", wallets[1].SEP10ClientDomain) + assert.Equal(t, "vibrantapp.com", wallets[1].SEP10ClientDomain) assert.Equal(t, "https://aidpubnet.netlify.app", wallets[1].DeepLinkSchema) expectedLogs := []string{ @@ -142,7 +142,7 @@ func Test_SetupWalletsForProperNetwork(t *testing.T) { "Name: Vibrant Assist", "Homepage: https://vibrantapp.com/vibrant-assist", "Deep Link Schema: https://aidpubnet.netlify.app", - "SEP-10 Client Domain: api.vibrantapp.com", + "SEP-10 Client Domain: vibrantapp.com", } logs := buf.String() @@ -164,7 +164,7 @@ func Test_SetupWalletsForProperNetwork(t *testing.T) { Name: "Vibrant Assist", Homepage: "https://vibrantapp.com/vibrant-assist", DeepLinkSchema: "https://aidpubnet.netlify.app", - SEP10ClientDomain: "api.vibrantapp.com", + SEP10ClientDomain: "vibrantapp.com", Assets: []data.Asset{ { Code: "USDC", @@ -253,7 +253,7 @@ func Test_SetupWalletsForProperNetwork(t *testing.T) { "Name: Vibrant Assist", "Homepage: https://vibrantapp.com/vibrant-assist", "Deep Link Schema: https://aidpubnet.netlify.app", - "SEP-10 Client Domain: api.vibrantapp.com", + "SEP-10 Client Domain: vibrantapp.com", "Assets:", "* USDC - GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", } diff --git a/internal/services/wallets/wallets_pubnet.go b/internal/services/wallets/wallets_pubnet.go index 772c8b6e8..22e31c28f 100644 --- a/internal/services/wallets/wallets_pubnet.go +++ b/internal/services/wallets/wallets_pubnet.go @@ -10,7 +10,7 @@ var PubnetWallets = []data.Wallet{ Name: "Vibrant Assist", Homepage: "https://vibrantapp.com/vibrant-assist", DeepLinkSchema: "https://vibrantapp.com/sdp", - SEP10ClientDomain: "api.vibrantapp.com", + SEP10ClientDomain: "vibrantapp.com", Assets: []data.Asset{ assets.USDCAssetPubnet, }, diff --git a/internal/transactionsubmission/utils/errors.go b/internal/transactionsubmission/utils/errors.go index 1a85d29c2..68a14fa21 100644 --- a/internal/transactionsubmission/utils/errors.go +++ b/internal/transactionsubmission/utils/errors.go @@ -67,7 +67,7 @@ func NewHorizonErrorWrapper(err error) *HorizonErrorWrapper { resultCodes, resCodeErr := hError.ResultCodes() if resCodeErr != nil { - log.Errorf("parsing result_codes: %v", resCodeErr) + log.Warnf("parsing result_codes: %v", resCodeErr) } return &HorizonErrorWrapper{ diff --git a/main.go b/main.go index 4521fd22e..32771ad60 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( // Version is the official version of this application. Whenever it's changed // here, it also needs to be updated at the `helmchart/Chart.yaml#appVersionā€œ. -const Version = "1.0.0" +const Version = "1.0.1" // GitCommit is populated at build time by // go build -ldflags "-X main.GitCommit=$GIT_COMMIT"