Your information has been successfully verified!
+
+ Your account was verified using the following contact information:
+ {{.TruncatedContactInfo}}
+
Click the button below to be taken back to home and receive your
disbursement.
diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go
index 175c726f7..db4db08ae 100644
--- a/internal/serve/httphandler/payments_handler_test.go
+++ b/internal/serve/httphandler/payments_handler_test.go
@@ -1537,7 +1537,8 @@ func Test_PaymentsHandler_getPaymentsWithCount(t *testing.T) {
ReceiverWallet: receiverWallet,
})
- response, err := handler.getPaymentsWithCount(ctx, &data.QueryParams{})
+ params := data.QueryParams{SortBy: data.DefaultPaymentSortField, SortOrder: data.DefaultPaymentSortOrder}
+ response, err := handler.getPaymentsWithCount(ctx, ¶ms)
require.NoError(t, err)
assert.Equal(t, response.Total, 2)
diff --git a/internal/serve/httphandler/receiver_registration.go b/internal/serve/httphandler/receiver_registration.go
index fbdd82178..a2201c45f 100644
--- a/internal/serve/httphandler/receiver_registration.go
+++ b/internal/serve/httphandler/receiver_registration.go
@@ -11,6 +11,7 @@ import (
"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
htmlTpl "github.com/stellar/stellar-disbursement-platform-backend/internal/htmltemplate"
"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
type ReceiverRegistrationHandler struct {
@@ -20,13 +21,14 @@ type ReceiverRegistrationHandler struct {
}
type ReceiverRegistrationData struct {
- StellarAccount string
- JWTToken string
- Title string
- Message string
- ReCAPTCHASiteKey string
- PrivacyPolicyLink string
- OrganizationName string
+ StellarAccount string
+ JWTToken string
+ Title string
+ Message string
+ ReCAPTCHASiteKey string
+ PrivacyPolicyLink string
+ OrganizationName string
+ TruncatedContactInfo string
}
// ServeHTTP will serve the SEP-24 deposit page needed to register users.
@@ -86,6 +88,7 @@ func (h ReceiverRegistrationHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
htmlTemplateName = "receiver_registered_successfully.tmpl"
tmplData.Title = "Registration Complete ๐"
tmplData.Message = "Your Stellar wallet has been registered successfully!"
+ tmplData.TruncatedContactInfo = utils.TruncateString(rw.OTPConfirmedWith, 3)
}
registerPage, err := htmlTpl.ExecuteHTMLTemplate(htmlTemplateName, tmplData)
diff --git a/internal/serve/httphandler/verify_receiver_registration_handler.go b/internal/serve/httphandler/verify_receiver_registration_handler.go
index 8efba221a..0f39c145f 100644
--- a/internal/serve/httphandler/verify_receiver_registration_handler.go
+++ b/internal/serve/httphandler/verify_receiver_registration_handler.go
@@ -189,6 +189,7 @@ func (v VerifyReceiverRegistrationHandler) processReceiverWalletOTP(
dbTx db.DBTransaction,
sep24Claims anchorplatform.SEP24JWTClaims,
receiver data.Receiver, otp string,
+ contactInfo string,
) (receiverWallet data.ReceiverWallet, wasAlreadyRegistered bool, err error) {
// STEP 1: find the receiver wallet for the given [receiverID, clientDomain]
rw, err := v.Models.ReceiverWallet.GetByReceiverIDAndWalletDomain(ctx, receiver.ID, sep24Claims.ClientDomain(), dbTx)
@@ -220,6 +221,7 @@ func (v VerifyReceiverRegistrationHandler) processReceiverWalletOTP(
// STEP 5: update receiver wallet status to "REGISTERED"
now := time.Now()
rw.OTPConfirmedAt = &now
+ rw.OTPConfirmedWith = contactInfo
rw.Status = data.RegisteredReceiversWalletStatus
rw.StellarAddress = sep24Claims.SEP10StellarAccount()
rw.StellarMemo = sep24Claims.SEP10StellarMemo()
@@ -310,7 +312,7 @@ func (v VerifyReceiverRegistrationHandler) VerifyReceiverRegistration(w http.Res
}
// STEP 4: process OTP
- receiverWallet, wasAlreadyRegistered, err := v.processReceiverWalletOTP(ctx, dbTx, *sep24Claims, *receiver, receiverRegistrationRequest.OTP)
+ receiverWallet, wasAlreadyRegistered, err := v.processReceiverWalletOTP(ctx, dbTx, *sep24Claims, *receiver, receiverRegistrationRequest.OTP, contactInfo)
if err != nil {
return nil, fmt.Errorf("processing OTP for receiver with contact info %s: %w", truncatedContactInfo, err)
}
diff --git a/internal/serve/httphandler/verify_receiver_registration_handler_test.go b/internal/serve/httphandler/verify_receiver_registration_handler_test.go
index 2e67b56c3..040cf985e 100644
--- a/internal/serve/httphandler/verify_receiver_registration_handler_test.go
+++ b/internal/serve/httphandler/verify_receiver_registration_handler_test.go
@@ -427,6 +427,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverWalletOTP(t *testing.
if !tc.shouldOTPMatch {
otp = wrongOTP
}
+ receiverEmail := "test@stellar.org"
// receiver & receiver wallet
receiver := data.CreateReceiverFixture(t, ctx, dbTx, &data.Receiver{PhoneNumber: "+380445555555"})
@@ -435,23 +436,25 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverWalletOTP(t *testing.
receiverWallet = data.CreateReceiverWalletFixture(t, ctx, dbTx, receiver.ID, wallet.ID, tc.currentReceiverWalletStatus)
var stellarAddress string
var otpConfirmedAt *time.Time
+ var otpConfirmedWith string
if tc.wantWasAlreadyRegistered {
stellarAddress = "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444"
now := time.Now()
otpConfirmedAt = &now
+ otpConfirmedWith = receiverEmail
}
const q = `
UPDATE receiver_wallets
- SET otp = $1, otp_created_at = NOW(), stellar_address = $2, otp_confirmed_at = $3
- WHERE id = $4
+ SET otp = $1, otp_created_at = NOW(), stellar_address = $2, otp_confirmed_at = $3, otp_confirmed_with = $4
+ WHERE id = $5
`
- _, err = dbTx.ExecContext(ctx, q, correctOTP, sql.NullString{String: stellarAddress, Valid: stellarAddress != ""}, otpConfirmedAt, receiverWallet.ID)
+ _, err = dbTx.ExecContext(ctx, q, correctOTP, sql.NullString{String: stellarAddress, Valid: stellarAddress != ""}, otpConfirmedAt, otpConfirmedWith, receiverWallet.ID)
require.NoError(t, err)
}
// assertions
- rwUpdated, wasAlreadyRegistered, err := handler.processReceiverWalletOTP(ctx, dbTx, *tc.sep24Claims, *receiver, otp)
+ rwUpdated, wasAlreadyRegistered, err := handler.processReceiverWalletOTP(ctx, dbTx, *tc.sep24Claims, *receiver, otp, receiverEmail)
if tc.wantErrContains == nil {
require.NoError(t, err)
assert.Equal(t, tc.wantWasAlreadyRegistered, wasAlreadyRegistered)
@@ -466,6 +469,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverWalletOTP(t *testing.
assert.Equal(t, rwUpdated.StellarAddress, rw.StellarAddress)
assert.NotNil(t, rw.OTPConfirmedAt)
assert.NotNil(t, rwUpdated.OTPConfirmedAt)
+ assert.Equal(t, rwUpdated.OTPConfirmedWith, receiverEmail)
assert.WithinDuration(t, *rwUpdated.OTPConfirmedAt, *rw.OTPConfirmedAt, time.Millisecond)
} else {
From b7c47bdc12bd3383259bd341642f4aa1c14c9539 Mon Sep 17 00:00:00 2001
From: Marwen Abid
Date: Tue, 17 Sep 2024 15:56:58 -0700
Subject: [PATCH 31/75] SDP-1343 Fix HTML escaping (#420)
---
internal/htmltemplate/htmltemplate.go | 2 +-
internal/htmltemplate/htmltemplate_test.go | 3 ++-
internal/message/aws_ses_client.go | 3 ++-
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/internal/htmltemplate/htmltemplate.go b/internal/htmltemplate/htmltemplate.go
index c3170918b..bf89d4755 100644
--- a/internal/htmltemplate/htmltemplate.go
+++ b/internal/htmltemplate/htmltemplate.go
@@ -26,7 +26,7 @@ func ExecuteHTMLTemplate(templateName string, data interface{}) (string, error)
}
type EmptyBodyEmailTemplate struct {
- Body string
+ Body template.HTML
}
func ExecuteHTMLTemplateForEmailEmptyBody(data EmptyBodyEmailTemplate) (string, error) {
diff --git a/internal/htmltemplate/htmltemplate_test.go b/internal/htmltemplate/htmltemplate_test.go
index 667e33483..7c92462eb 100644
--- a/internal/htmltemplate/htmltemplate_test.go
+++ b/internal/htmltemplate/htmltemplate_test.go
@@ -3,6 +3,7 @@ package htmltemplate
import (
"crypto/rand"
"fmt"
+ "html/template"
"testing"
"github.com/stretchr/testify/assert"
@@ -43,7 +44,7 @@ func Test_ExecuteHTMLTemplateForEmailEmptyBody(t *testing.T) {
randomStr := fmt.Sprintf("%x", b)[:10]
// check if the random string is imprinted in the template
- inputData := EmptyBodyEmailTemplate{Body: randomStr}
+ inputData := EmptyBodyEmailTemplate{Body: template.HTML(randomStr)}
templateStr, err := ExecuteHTMLTemplateForEmailEmptyBody(inputData)
require.NoError(t, err)
require.Contains(t, templateStr, randomStr)
diff --git a/internal/message/aws_ses_client.go b/internal/message/aws_ses_client.go
index cb0111689..9a3e798c7 100644
--- a/internal/message/aws_ses_client.go
+++ b/internal/message/aws_ses_client.go
@@ -2,6 +2,7 @@ package message
import (
"fmt"
+ "html/template"
"strings"
"github.com/aws/aws-sdk-go/aws"
@@ -51,7 +52,7 @@ func (a *awsSESClient) SendMessage(message Message) error {
// generateAWSEmail generates the email object to send an email through AWS SES.
func generateAWSEmail(message Message, sender string) (*ses.SendEmailInput, error) {
- html, err := htmltemplate.ExecuteHTMLTemplateForEmailEmptyBody(htmltemplate.EmptyBodyEmailTemplate{Body: message.Message})
+ html, err := htmltemplate.ExecuteHTMLTemplateForEmailEmptyBody(htmltemplate.EmptyBodyEmailTemplate{Body: template.HTML(message.Message)})
if err != nil {
return nil, fmt.Errorf("generating html template: %w", err)
}
From 8571312d52d45832a3c650998b85f99375c462aa Mon Sep 17 00:00:00 2001
From: Benjamin VanEvery <821115+papaben@users.noreply.github.com>
Date: Wed, 25 Sep 2024 18:58:37 -0400
Subject: [PATCH 32/75] #421 Enhance onboarding safety checks (#423)
* jq and docker are not system defaults
* Check that /etc/hosts records were added for each tenant
Co-authored-by: Benjamin VanEvery
---
dev/main.sh | 21 +++++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/dev/main.sh b/dev/main.sh
index ac2dd5781..7ae2358cc 100755
--- a/dev/main.sh
+++ b/dev/main.sh
@@ -25,10 +25,18 @@ if [ ! -f ./.env ]; then
exit 1
fi
-# Check if curl is installed
-if ! command -v curl &> /dev/null
-then
- echo "Error: curl is not installed. Please install curl to continue."
+declare -a required_tools=( docker curl jq )
+declare failed=0
+
+for tool in ${required_tools[@]}; do
+ if ! command -v $tool &> /dev/null
+ then
+ echo "Error: $tool is not installed. Please install $tool to continue."
+ failed=1
+ fi
+done
+
+if [[ $failed != 0 ]]; then
exit 1
fi
@@ -153,4 +161,9 @@ for tenant in "${tenants[@]}"; do
url="http://$tenant.stellar.local:3000"
echo -e "๐Tenant $tenant: \033]8;;$url\033\\$url\033]8;;\033\\"
echo "username: owner@$tenant.local password: Password123!"
+
+ if ! grep -q $tenant /etc/hosts; then
+ echo >&2 "WARN $tenant.stellar.local missing from /etc/hosts"
+ fi
done
+
From 6ae2d5077f790bc43287c2c65469b8de8614d27f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 9 Oct 2024 21:40:03 +0000
Subject: [PATCH 33/75] Bump the all-actions group across 1 directory with 2
updates (#429)
---
.github/workflows/ci.yml | 2 +-
.github/workflows/docker_image_public_release.yml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 666456ab5..e7d24c354 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,7 @@ jobs:
cache-dependency-path: go.sum
- name: golangci-lint
- uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # version v6.1.0
+ uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # version v6.1.1
with:
version: v1.56.2 # this is the golangci-lint version
args: --timeout 5m0s
diff --git a/.github/workflows/docker_image_public_release.yml b/.github/workflows/docker_image_public_release.yml
index 61e89daac..a098e903a 100644
--- a/.github/workflows/docker_image_public_release.yml
+++ b/.github/workflows/docker_image_public_release.yml
@@ -60,7 +60,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to DockerHub (release prd)
- uses: docker/build-push-action@v6.7.0
+ uses: docker/build-push-action@v6.9.0
with:
push: true
build-args: |
@@ -95,7 +95,7 @@ jobs:
run: echo "SHA=$(git rev-parse --short ${{ github.sha }} )" >> $GITHUB_OUTPUT
- name: Build and push to DockerHub (develop branch)
- uses: docker/build-push-action@v6.7.0
+ uses: docker/build-push-action@v6.9.0
with:
push: true
build-args: |
From bac423315213bd5a459b559f56882a7b96731058 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 9 Oct 2024 21:48:55 +0000
Subject: [PATCH 34/75] Bump golang in the all-docker group (#430)
---
Dockerfile | 2 +-
Dockerfile.development | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 821dcc658..d6f40831d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@
# To push:
# make docker-push
-FROM golang:1.23.1-bullseye AS build
+FROM golang:1.23.2-bullseye AS build
ARG GIT_COMMIT
WORKDIR /src/stellar-disbursement-platform
diff --git a/Dockerfile.development b/Dockerfile.development
index 47eb05049..a4597de90 100644
--- a/Dockerfile.development
+++ b/Dockerfile.development
@@ -1,5 +1,5 @@
# Stage 1: Build the Go application
-FROM golang:1.23.1-bullseye AS build
+FROM golang:1.23.2-bullseye AS build
ARG GIT_COMMIT
WORKDIR /src/stellar-disbursement-platform
@@ -9,7 +9,7 @@ COPY . ./
RUN go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=$GIT_COMMIT" .
# Stage 2: Setup the development environment with Delve for debugging
-FROM golang:1.23.1-bullseye AS development
+FROM golang:1.23.2-bullseye AS development
# set workdir according to repo structure so remote debug source code is in sync
WORKDIR /app/github.com/stellar/stellar-disbursement-platform
From cdf79f2b4a0e510cf8612adabdca75d93358fb45 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 9 Oct 2024 22:01:14 +0000
Subject: [PATCH 35/75] Bump the minor-and-patch group across 1 directory with
4 updates (#431)
---
go.list | 16 +++++++---------
go.mod | 12 ++++++------
go.sum | 24 ++++++++++++------------
3 files changed, 25 insertions(+), 27 deletions(-)
diff --git a/go.list b/go.list
index f903e4302..713439d2f 100644
--- a/go.list
+++ b/go.list
@@ -43,7 +43,6 @@ github.com/cespare/xxhash/v2 v2.3.0
github.com/chzyer/logex v1.2.1
github.com/chzyer/readline v1.5.1
github.com/chzyer/test v1.0.0
-github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0
github.com/coreos/go-semver v0.3.0
github.com/coreos/go-systemd/v22 v22.3.2
github.com/cpuguy83/go-md2man/v2 v2.0.4
@@ -64,7 +63,7 @@ github.com/fsnotify/fsnotify v1.7.0
github.com/fsouza/fake-gcs-server v1.49.0
github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955
github.com/getsentry/raven-go v0.2.0
-github.com/getsentry/sentry-go v0.28.1
+github.com/getsentry/sentry-go v0.29.0
github.com/gin-contrib/sse v0.1.0
github.com/gin-gonic/gin v1.8.1 => github.com/gin-gonic/gin v1.9.1
github.com/go-chi/chi v4.1.2+incompatible
@@ -76,7 +75,6 @@ github.com/go-kit/log v0.2.1
github.com/go-logfmt/logfmt v0.6.0
github.com/go-logr/logr v1.4.1
github.com/go-logr/stdr v1.2.2
-github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab
github.com/go-playground/locales v0.14.0
github.com/go-playground/universal-translator v0.18.0
github.com/go-playground/validator/v10 v10.11.1
@@ -201,7 +199,7 @@ github.com/pkg/xattr v0.4.9
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/posener/complete v1.2.3
github.com/poy/onpar v1.1.2
-github.com/prometheus/client_golang v1.20.3
+github.com/prometheus/client_golang v1.20.4
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.55.0
github.com/prometheus/procfs v0.15.1
@@ -235,7 +233,7 @@ github.com/stretchr/testify v1.9.0
github.com/subosito/gotenv v1.6.0
github.com/tdewolff/minify/v2 v2.12.4
github.com/tdewolff/parse/v2 v2.6.4
-github.com/twilio/twilio-go v1.23.0
+github.com/twilio/twilio-go v1.23.3
github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8
github.com/ugorji/go/codec v1.2.7
github.com/urfave/negroni v1.0.0
@@ -272,15 +270,15 @@ go.opentelemetry.io/otel/trace v1.24.0
go.uber.org/atomic v1.9.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.21.0
-golang.org/x/crypto v0.27.0
+golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
golang.org/x/mod v0.17.0
golang.org/x/net v0.26.0
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.8.0
-golang.org/x/sys v0.25.0
-golang.org/x/term v0.24.0
-golang.org/x/text v0.18.0
+golang.org/x/sys v0.26.0
+golang.org/x/term v0.25.0
+golang.org/x/text v0.19.0
golang.org/x/time v0.5.0
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
diff --git a/go.mod b/go.mod
index dc0586f4f..0cc63c039 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/avast/retry-go v3.0.0+incompatible
github.com/aws/aws-sdk-go v1.55.5
- github.com/getsentry/sentry-go v0.28.1
+ github.com/getsentry/sentry-go v0.29.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/httprate v0.14.1
@@ -19,7 +19,7 @@ require (
github.com/lib/pq v1.10.9
github.com/manifoldco/promptui v0.9.0
github.com/nyaruka/phonenumbers v1.4.0
- github.com/prometheus/client_golang v1.20.3
+ github.com/prometheus/client_golang v1.20.4
github.com/rs/cors v1.11.1
github.com/rubenv/sql-migrate v1.7.0
github.com/segmentio/kafka-go v0.4.47
@@ -28,8 +28,8 @@ require (
github.com/spf13/viper v1.19.0
github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043
github.com/stretchr/testify v1.9.0
- github.com/twilio/twilio-go v1.23.0
- golang.org/x/crypto v0.27.0
+ github.com/twilio/twilio-go v1.23.3
+ golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
)
@@ -71,8 +71,8 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/sys v0.25.0 // indirect
- golang.org/x/text v0.18.0 // indirect
+ golang.org/x/sys v0.26.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tylerb/graceful.v1 v1.2.15 // indirect
diff --git a/go.sum b/go.sum
index 643c1939d..2d0d09066 100644
--- a/go.sum
+++ b/go.sum
@@ -40,8 +40,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4=
github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA=
-github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
-github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
+github.com/getsentry/sentry-go v0.29.0 h1:YtWluuCFg9OfcqnaujpY918N/AhCCwarIDWOYSBAjCA=
+github.com/getsentry/sentry-go v0.29.0/go.mod h1:jhPesDAL0Q0W2+2YEuVOvdWmVtdsr1+jtBrlDEVWwLY=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
@@ -138,8 +138,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
-github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
-github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
+github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
@@ -195,8 +195,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/twilio/twilio-go v1.23.0 h1:cIJD6XnVuRqnMVp8LswoOTEi4/JK9WctOTUvUR2gLf0=
-github.com/twilio/twilio-go v1.23.0/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko=
+github.com/twilio/twilio-go v1.23.3 h1:9DsuC9+6CfQW9dlzdeQeyhn3z2oPjZQcOhMCgh5VkgE=
+github.com/twilio/twilio-go v1.23.3/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
@@ -229,8 +229,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4=
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -266,8 +266,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -280,8 +280,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
From 25a8dc98c1720ac8b935726e1047790659ed0fac Mon Sep 17 00:00:00 2001
From: Marwen Abid
Date: Mon, 14 Oct 2024 07:17:26 -0700
Subject: [PATCH 36/75] Chore: add list of pre-requisites needed to run the SDP
setup scripts (#428)
### What
* Add the tools needed to run the pre-requisites. Docker, Golang and JQ.
### Why
Reduce friction during setup.
---
dev/README.md | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/dev/README.md b/dev/README.md
index 5a15af089..f100ac3b6 100644
--- a/dev/README.md
+++ b/dev/README.md
@@ -31,9 +31,15 @@ Follow these instructions to get started with the Stellar Disbursement Platform
## Quick Setup and Deployment
-### Docker
-
-Make sure you have Docker installed on your system. If not, you can download it from [here](https://www.docker.com/products/docker-desktop).
+### Pre-requisites
+
+* **Docker:** Make sure you have Docker installed on your system. If not, you can download it from [here](https://www.docker.com/products/docker-desktop).
+* **Git:** You will need Git to clone the repository. You can download it from [here](https://git-scm.com/downloads).
+* **Go:** If you want to use the `make_env.sh` script to create Stellar accounts and a `.env` file, you will need to have Go installed on your system. You can download it from [here](https://golang.org/dl/).
+* **jq:** If you want to use the `main.sh` script to bring up the local environment, you will need to have `jq` installed. You can install it using Homebrew:
+```sh
+brew install jq
+```
### Clone the repository:
From 1f4dc11fc0f4ebcf0f20972b57da3c86515f36f2 Mon Sep 17 00:00:00 2001
From: Marwen Abid
Date: Mon, 14 Oct 2024 07:49:18 -0700
Subject: [PATCH 37/75] Update changelog and bump version for 3.0.0-rc.1 (#424)
### What
Release `3.0.0-rc.1`
---
CHANGELOG.md | 25 +++++++++++++++++++
helmchart/sdp/Chart.yaml | 4 +--
helmchart/sdp/README.md | 4 +--
.../sdp/templates/01.1-configmap-sdp.yaml | 2 +-
helmchart/sdp/values.yaml | 4 +--
main.go | 2 +-
6 files changed, 33 insertions(+), 8 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 089a883ef..e7f4e2449 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
## Unreleased
+## [3.0.0-rc.1](https://github.com/stellar/stellar-disbursement-platform-backend/releases/tag/3.0.0-rc.1) ([diff](https://github.com/stellar/stellar-disbursement-platform-backend/compare/2.1.0...3.0.0-rc.1))
+
+Release of the Stellar Disbursement Platform v3.0.0-rc.1. This release introduces
+the option to register receivers using email addresses, in addition to phone numbers.
+
### Breaking Changes
- Renamed properties and environment variables related to Email Registration Support [#412](https://github.com/stellar/stellar-disbursement-platform-backend/pull/412)
- Renamed `MAX_INVITATION_SMS_RESEND_ATTEMPT` environment variable to `MAX_INVITATION_RESEND_ATTEMPTS`
@@ -13,6 +18,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
- Renamed `organization.sms_registration_message_template` to `organization.receiver_registration_message_template`
- Renamed `disbursement.sms_registration_message_template` to `disbursement.receiver_registration_message_template`
+### Added
+
+- Ability to register receivers using email addresses
+ - Update the receiver_registered_successfully.tmpl HTML template to display the contact info [#418](https://github.com/stellar/stellar-disbursement-platform-backend/pull/418)
+ - Update `/wallet-registration/verification` to accommodate different verification methods [#416](https://github.com/stellar/stellar-disbursement-platform-backend/pull/416)
+ - Update send and auto-retry invitation scheduler job to work with email [#415](https://github.com/stellar/stellar-disbursement-platform-backend/pull/415)
+ - Update `POST /wallet-registration/otp` to send OTPs through email [#413](https://github.com/stellar/stellar-disbursement-platform-backend/pull/413)
+ - Rename SMS related fields in `organization` and `disbursement` to be more generic [#412](https://github.com/stellar/stellar-disbursement-platform-backend/pull/412)
+ - Update process disbursement instructions to accept email addresses [#404](https://github.com/stellar/stellar-disbursement-platform-backend/pull/404)
+ - Add initial screen so receivers can choose between phone number and email registration during registration [#406](https://github.com/stellar/stellar-disbursement-platform-backend/pull/406)
+ - Add message channel priority or `organizations` table [#400](https://github.com/stellar/stellar-disbursement-platform-backend/pull/400)
+ - Add MessageDispatcher to SDP to send messages to different channels [#391](https://github.com/stellar/stellar-disbursement-platform-backend/pull/391)
+
+### Security and Dependencies
+
+- Fix HTML Injection Vulnerability [#419](https://github.com/stellar/stellar-disbursement-platform-backend/pull/419), Fix HTML escaping [#420](https://github.com/stellar/stellar-disbursement-platform-backend/pull/420)
+- Bump `golangci/golangci-lint-action` [#380](https://github.com/stellar/stellar-disbursement-platform-backend/pull/380)
+- Bump `golang` in the all-docker group [#387](https://github.com/stellar/stellar-disbursement-platform-backend/pull/387), [#394](https://github.com/stellar/stellar-disbursement-platform-backend/pull/394), [#414](https://github.com/stellar/stellar-disbursement-platform-backend/pull/414)
+- Bump minor and patch dependencies across directories [#381](https://github.com/stellar/stellar-disbursement-platform-backend/pull/381), [#395](https://github.com/stellar/stellar-disbursement-platform-backend/pull/395), [#403](https://github.com/stellar/stellar-disbursement-platform-backend/pull/403), [#411](https://github.com/stellar/stellar-disbursement-platform-backend/pull/411)
+
## [2.1.1](https://github.com/stellar/stellar-disbursement-platform-backend/releases/tag/2.1.1) ([diff](https://github.com/stellar/stellar-disbursement-platform-backend/compare/2.1.0...2.1.1))
### Changed
diff --git a/helmchart/sdp/Chart.yaml b/helmchart/sdp/Chart.yaml
index 5b945f8bb..92e0e3711 100644
--- a/helmchart/sdp/Chart.yaml
+++ b/helmchart/sdp/Chart.yaml
@@ -1,8 +1,8 @@
apiVersion: v2
name: stellar-disbursement-platform
description: A Helm chart for the Stellar Disbursement Platform Backend (A.K.A. `sdp`)
-version: "2.1.1"
-appVersion: "2.1.1"
+version: "3.0.0-rc.1"
+appVersion: "3.0.0-rc.1"
type: application
maintainers:
- name: Stellar Development Foundation
diff --git a/helmchart/sdp/README.md b/helmchart/sdp/README.md
index 690381208..a1965a58f 100644
--- a/helmchart/sdp/README.md
+++ b/helmchart/sdp/README.md
@@ -106,7 +106,7 @@ Configuration parameters for the SDP Core Service which is the core backend serv
| `sdp.image` | Configuration related to the Docker image used by the SDP service. | |
| `sdp.image.repository` | Docker image repository for the SDP backend service. | `stellar/stellar-disbursement-platform-backend` |
| `sdp.image.pullPolicy` | Image pull policy for the SDP service. For locally built images, consider using "Never" or "IfNotPresent". | `Always` |
-| `sdp.image.tag` | Docker image tag for the SDP service. If set, this overrides the default value from `.Chart.AppVersion`. | `latest` |
+| `sdp.image.tag` | Docker image tag for the SDP service. If set, this overrides the default value from `.Chart.AppVersion`. | `3.0.0-rc.1` |
| `sdp.deployment` | Configuration related to the deployment of the SDP service. | |
| `sdp.deployment.annotations` | Annotations to be added to the deployment. | `nil` |
| `sdp.deployment.podAnnotations` | Annotations specific to the pods. | `{}` |
@@ -289,7 +289,7 @@ Configuration parameters for the Dashboard. This is the user interface administr
| `dashboard.route.mtnDomain` | Public domain/address of the multi-tenant Dashboard. This is a wild-card domain used for multi-tenant setups e.g. "*.sdp-dashboard.localhost.com". | `nil` |
| `dashboard.route.port` | Primary port on which the Dashboard listens. | `80` |
| `dashboard.image` | Configuration related to the Docker image used by the Dashboard. | |
-| `dashboard.image.fullName` | Full name of the Docker image. | `stellar/stellar-disbursement-platform-frontend:latest` |
+| `dashboard.image.fullName` | Full name of the Docker image. | `stellar/stellar-disbursement-platform-frontend:3.0.0-rc.1` |
| `dashboard.image.pullPolicy` | Image pull policy for the dashboard. For locally built images, consider using "Never" or "IfNotPresent". | `Always` |
| `dashboard.deployment` | Configuration related to the deployment of the Dashboard. | |
| `dashboard.deployment.annotations` | Annotations to be added to the deployment. | `{}` |
diff --git a/helmchart/sdp/templates/01.1-configmap-sdp.yaml b/helmchart/sdp/templates/01.1-configmap-sdp.yaml
index bd469ec62..eee4b16cf 100644
--- a/helmchart/sdp/templates/01.1-configmap-sdp.yaml
+++ b/helmchart/sdp/templates/01.1-configmap-sdp.yaml
@@ -32,7 +32,7 @@ data:
CONSUMER_GROUP_ID: {{ .Values.global.eventBroker.consumerGroupId | quote }}
{{- if eq .Values.global.eventBroker.type "KAFKA" }}
KAFKA_SECURITY_PROTOCOL: {{ .Values.global.eventBroker.kafka.securityProtocol | quote }}
- SINGLE_TENANT_MODE: {{ .Values.global.singleTenantMode | quote }}
{{- end }}
+ SINGLE_TENANT_MODE: {{ .Values.global.singleTenantMode | quote }}
{{- tpl (toYaml .Values.sdp.configMap.data | nindent 2) . }}
{{- end }}
diff --git a/helmchart/sdp/values.yaml b/helmchart/sdp/values.yaml
index 58e7bd77e..2fae64a69 100644
--- a/helmchart/sdp/values.yaml
+++ b/helmchart/sdp/values.yaml
@@ -111,7 +111,7 @@ sdp:
image:
repository: stellar/stellar-disbursement-platform-backend
pullPolicy: Always
- tag: "latest"
+ tag: "3.0.0-rc.1"
## @extra sdp.deployment Configuration related to the deployment of the SDP service.
## @param sdp.deployment.annotations Annotations to be added to the deployment.
@@ -532,7 +532,7 @@ dashboard:
## @param dashboard.image.fullName Full name of the Docker image.
## @param dashboard.image.pullPolicy Image pull policy for the dashboard. For locally built images, consider using "Never" or "IfNotPresent".
image:
- fullName: stellar/stellar-disbursement-platform-frontend:latest
+ fullName: stellar/stellar-disbursement-platform-frontend:3.0.0-rc.1
pullPolicy: Always
## @extra dashboard.deployment Configuration related to the deployment of the Dashboard.
diff --git a/main.go b/main.go
index ce517a4a9..3c7ec0d3f 100644
--- a/main.go
+++ b/main.go
@@ -13,7 +13,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 = "2.1.1"
+const Version = "3.0.0-rc.1"
// GitCommit is populated at build time by
// go build -ldflags "-X main.GitCommit=$GIT_COMMIT"
From d2779677a3229cc513d5bbc1f5ae5324b9cd6562 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 21 Oct 2024 12:53:16 -0700
Subject: [PATCH 38/75] Bump the minor-and-patch group with 4 updates (#441)
* Bump the minor-and-patch group with 4 updates
Bumps the minor-and-patch group with 4 updates: [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go), [github.com/nyaruka/phonenumbers](https://github.com/nyaruka/phonenumbers), [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) and [github.com/twilio/twilio-go](https://github.com/twilio/twilio-go).
Updates `github.com/getsentry/sentry-go` from 0.29.0 to 0.29.1
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.29.0...v0.29.1)
Updates `github.com/nyaruka/phonenumbers` from 1.4.0 to 1.4.1
- [Release notes](https://github.com/nyaruka/phonenumbers/releases)
- [Changelog](https://github.com/nyaruka/phonenumbers/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nyaruka/phonenumbers/compare/v1.4.0...v1.4.1)
Updates `github.com/prometheus/client_golang` from 1.20.4 to 1.20.5
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.4...v1.20.5)
Updates `github.com/twilio/twilio-go` from 1.23.3 to 1.23.4
- [Release notes](https://github.com/twilio/twilio-go/releases)
- [Changelog](https://github.com/twilio/twilio-go/blob/main/CHANGES.md)
- [Commits](https://github.com/twilio/twilio-go/compare/v1.23.3...v1.23.4)
---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: minor-and-patch
- dependency-name: github.com/nyaruka/phonenumbers
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: minor-and-patch
- dependency-name: github.com/prometheus/client_golang
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: minor-and-patch
- dependency-name: github.com/twilio/twilio-go
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: minor-and-patch
...
Signed-off-by: dependabot[bot]
* Update go.list.
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marcelo Salloum
---
go.list | 10 +++++-----
go.mod | 8 ++++----
go.sum | 16 ++++++++--------
3 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/go.list b/go.list
index 713439d2f..979f929dd 100644
--- a/go.list
+++ b/go.list
@@ -63,7 +63,7 @@ github.com/fsnotify/fsnotify v1.7.0
github.com/fsouza/fake-gcs-server v1.49.0
github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955
github.com/getsentry/raven-go v0.2.0
-github.com/getsentry/sentry-go v0.29.0
+github.com/getsentry/sentry-go v0.29.1
github.com/gin-contrib/sse v0.1.0
github.com/gin-gonic/gin v1.8.1 => github.com/gin-gonic/gin v1.9.1
github.com/go-chi/chi v4.1.2+incompatible
@@ -184,7 +184,7 @@ github.com/nats-io/nuid v1.0.1
github.com/nelsam/hel/v2 v2.3.3
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e
github.com/nxadm/tail v1.4.11
-github.com/nyaruka/phonenumbers v1.4.0
+github.com/nyaruka/phonenumbers v1.4.1
github.com/olekukonko/tablewriter v0.0.5
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.27.10
@@ -199,7 +199,7 @@ github.com/pkg/xattr v0.4.9
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/posener/complete v1.2.3
github.com/poy/onpar v1.1.2
-github.com/prometheus/client_golang v1.20.4
+github.com/prometheus/client_golang v1.20.5
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.55.0
github.com/prometheus/procfs v0.15.1
@@ -233,10 +233,10 @@ github.com/stretchr/testify v1.9.0
github.com/subosito/gotenv v1.6.0
github.com/tdewolff/minify/v2 v2.12.4
github.com/tdewolff/parse/v2 v2.6.4
-github.com/twilio/twilio-go v1.23.3
+github.com/twilio/twilio-go v1.23.4
github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8
github.com/ugorji/go/codec v1.2.7
-github.com/urfave/negroni v1.0.0
+github.com/urfave/negroni/v3 v3.1.1
github.com/valyala/bytebufferpool v1.0.0
github.com/valyala/fasthttp v1.52.0
github.com/valyala/fasttemplate v1.2.2
diff --git a/go.mod b/go.mod
index 0cc63c039..49df7aa76 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/avast/retry-go v3.0.0+incompatible
github.com/aws/aws-sdk-go v1.55.5
- github.com/getsentry/sentry-go v0.29.0
+ github.com/getsentry/sentry-go v0.29.1
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/httprate v0.14.1
@@ -18,8 +18,8 @@ require (
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/manifoldco/promptui v0.9.0
- github.com/nyaruka/phonenumbers v1.4.0
- github.com/prometheus/client_golang v1.20.4
+ github.com/nyaruka/phonenumbers v1.4.1
+ github.com/prometheus/client_golang v1.20.5
github.com/rs/cors v1.11.1
github.com/rubenv/sql-migrate v1.7.0
github.com/segmentio/kafka-go v0.4.47
@@ -28,7 +28,7 @@ require (
github.com/spf13/viper v1.19.0
github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043
github.com/stretchr/testify v1.9.0
- github.com/twilio/twilio-go v1.23.3
+ github.com/twilio/twilio-go v1.23.4
golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
)
diff --git a/go.sum b/go.sum
index 2d0d09066..8709e494a 100644
--- a/go.sum
+++ b/go.sum
@@ -40,8 +40,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4=
github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA=
-github.com/getsentry/sentry-go v0.29.0 h1:YtWluuCFg9OfcqnaujpY918N/AhCCwarIDWOYSBAjCA=
-github.com/getsentry/sentry-go v0.29.0/go.mod h1:jhPesDAL0Q0W2+2YEuVOvdWmVtdsr1+jtBrlDEVWwLY=
+github.com/getsentry/sentry-go v0.29.1 h1:DyZuChN8Hz3ARxGVV8ePaNXh1dQ7d76AiB117xcREwA=
+github.com/getsentry/sentry-go v0.29.1/go.mod h1:x3AtIzN01d6SiWkderzaH28Tm0lgkafpJ5Bm3li39O0=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
@@ -118,8 +118,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
-github.com/nyaruka/phonenumbers v1.4.0 h1:ddhWiHnHCIX3n6ETDA58Zq5dkxkjlvgrDWM2OHHPCzU=
-github.com/nyaruka/phonenumbers v1.4.0/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E=
+github.com/nyaruka/phonenumbers v1.4.1 h1:dNsiYGirahC2lMRz3p2dxmmyLbzD3arCgmj/hPEVRPY=
+github.com/nyaruka/phonenumbers v1.4.1/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
@@ -138,8 +138,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
-github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
-github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
@@ -195,8 +195,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/twilio/twilio-go v1.23.3 h1:9DsuC9+6CfQW9dlzdeQeyhn3z2oPjZQcOhMCgh5VkgE=
-github.com/twilio/twilio-go v1.23.3/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko=
+github.com/twilio/twilio-go v1.23.4 h1:1JePQ9xWVaW7iZieEIcfm1E89nOPcgZ+I5ZRcukBkRY=
+github.com/twilio/twilio-go v1.23.4/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
From 8557fbe3c4fe0337c1f67ec70e9f78da51052392 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 22 Oct 2024 10:07:21 -0700
Subject: [PATCH 39/75] [SDP-1302] `PATCH /receiver` now allows patching the
phone number and can handle conflicts (#436)
### What
`PATCH /receiver` now allows patching the phone number and can handle conflicts when the provided `phone_number` or `email` are already associated with another user.
Also, refactored the related tests to make them more reusable and to cover additional test cases.
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1302
---
internal/data/fixtures.go | 3 +
.../httphandler/update_receiver_handler.go | 35 +-
.../update_receiver_handler_test.go | 836 +++++++++---------
.../validators/receiver_update_validator.go | 9 +-
.../receiver_update_validator_test.go | 199 +++--
5 files changed, 546 insertions(+), 536 deletions(-)
diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go
index dc7c3760b..902c53f22 100644
--- a/internal/data/fixtures.go
+++ b/internal/data/fixtures.go
@@ -292,6 +292,9 @@ func CreateReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExec
randomSuffix, err := utils.RandomString(5)
require.NoError(t, err)
+ if r == nil {
+ r = &Receiver{}
+ }
if r.Email == "" {
r.Email = fmt.Sprintf("email%s@randomemail.com", randomSuffix)
}
diff --git a/internal/serve/httphandler/update_receiver_handler.go b/internal/serve/httphandler/update_receiver_handler.go
index db2571ddb..d0dae4a7f 100644
--- a/internal/serve/httphandler/update_receiver_handler.go
+++ b/internal/serve/httphandler/update_receiver_handler.go
@@ -4,8 +4,11 @@ import (
"errors"
"fmt"
"net/http"
+ "slices"
+ "strings"
"github.com/go-chi/chi/v5"
+ "github.com/lib/pq"
"github.com/stellar/go/support/http/httpdecode"
"github.com/stellar/go/support/log"
"github.com/stellar/go/support/render/httpjson"
@@ -85,7 +88,7 @@ func (h UpdateReceiverHandler) UpdateReceiver(rw http.ResponseWriter, req *http.
receiver, err := db.RunInTransactionWithResult(ctx, h.DBConnectionPool, nil, func(dbTx db.DBTransaction) (response *data.Receiver, innerErr error) {
for _, rv := range receiverVerifications {
innerErr = h.Models.ReceiverVerification.UpsertVerificationValue(
- req.Context(),
+ ctx,
dbTx,
rv.ReceiverID,
rv.VerificationField,
@@ -93,7 +96,7 @@ func (h UpdateReceiverHandler) UpdateReceiver(rw http.ResponseWriter, req *http.
)
if innerErr != nil {
- return nil, fmt.Errorf("error updating receiver verification %s: %w", rv.VerificationField, innerErr)
+ return nil, fmt.Errorf("updating receiver verification %s: %w", rv.VerificationField, innerErr)
}
}
@@ -101,6 +104,9 @@ func (h UpdateReceiverHandler) UpdateReceiver(rw http.ResponseWriter, req *http.
if reqBody.Email != "" {
receiverUpdate.Email = &reqBody.Email
}
+ if reqBody.PhoneNumber != "" {
+ receiverUpdate.PhoneNumber = &reqBody.PhoneNumber
+ }
if reqBody.ExternalID != "" {
receiverUpdate.ExternalId = &reqBody.ExternalID
}
@@ -113,15 +119,38 @@ func (h UpdateReceiverHandler) UpdateReceiver(rw http.ResponseWriter, req *http.
receiver, innerErr := h.Models.Receiver.Get(ctx, dbTx, receiverID)
if innerErr != nil {
- return nil, fmt.Errorf("error querying receiver with ID %s: %w", receiverID, innerErr)
+ return nil, fmt.Errorf("querying receiver with ID %s: %w", receiverID, innerErr)
}
return receiver, nil
})
if err != nil {
+ if httpErr := parseHttpConflictErrorIfNeeded(err); httpErr != nil {
+ httpErr.Render(rw)
+ return
+ }
+
httperror.InternalError(ctx, "", err, nil).Render(rw)
return
}
httpjson.Render(rw, receiver, httpjson.JSON)
}
+
+func parseHttpConflictErrorIfNeeded(err error) *httperror.HTTPError {
+ var pqErr *pq.Error
+ if err == nil || !errors.As(err, &pqErr) || pqErr.Code != "23505" {
+ return nil
+ }
+
+ allowedConstraints := []string{"receiver_unique_email", "receiver_unique_phone_number"}
+ if !slices.Contains(allowedConstraints, pqErr.Constraint) {
+ return nil
+ }
+ fieldName := strings.Replace(pqErr.Constraint, "receiver_unique_", "", 1)
+ msg := fmt.Sprintf("The provided %s is already associated with another user.", fieldName)
+
+ return httperror.Conflict(msg, err, map[string]interface{}{
+ fieldName: fieldName + " must be unique",
+ })
+}
diff --git a/internal/serve/httphandler/update_receiver_handler_test.go b/internal/serve/httphandler/update_receiver_handler_test.go
index d175f671a..ff33fced8 100644
--- a/internal/serve/httphandler/update_receiver_handler_test.go
+++ b/internal/serve/httphandler/update_receiver_handler_test.go
@@ -98,10 +98,9 @@ func Test_UpdateReceiverHandler_createVerificationInsert(t *testing.T) {
}
}
-func Test_UpdateReceiverHandler(t *testing.T) {
+func Test_UpdateReceiverHandler_400(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -115,26 +114,21 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
ctx := context.Background()
- receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{
- PhoneNumber: "+380445555555",
- Email: "receiver@email.com",
- ExternalID: "externalID",
- })
+ receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, nil)
// setup
r := chi.NewRouter()
r.Patch("/receivers/{id}", handler.UpdateReceiver)
- t.Run("error invalid request body", func(t *testing.T) {
- testCases := []struct {
- name string
- request validators.UpdateReceiverRequest
- want string
- }{
- {
- name: "empty request body",
- request: validators.UpdateReceiverRequest{},
- want: `
+ testCases := []struct {
+ name string
+ request validators.UpdateReceiverRequest
+ expectedBody string
+ }{
+ {
+ name: "empty request body",
+ request: validators.UpdateReceiverRequest{},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
@@ -142,11 +136,11 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
}
`,
- },
- {
- name: "invalid date of birth",
- request: validators.UpdateReceiverRequest{DateOfBirth: "invalid"},
- want: `
+ },
+ {
+ name: "invalid date of birth",
+ request: validators.UpdateReceiverRequest{DateOfBirth: "invalid"},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
@@ -154,11 +148,11 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
}
`,
- },
- {
- name: "invalid year/month",
- request: validators.UpdateReceiverRequest{YearMonth: "invalid"},
- want: `
+ },
+ {
+ name: "invalid year/month",
+ request: validators.UpdateReceiverRequest{YearMonth: "invalid"},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
@@ -166,11 +160,11 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
}
`,
- },
- {
- name: "invalid pin",
- request: validators.UpdateReceiverRequest{Pin: " "},
- want: `
+ },
+ {
+ name: "invalid pin",
+ request: validators.UpdateReceiverRequest{Pin: " "},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
@@ -178,11 +172,11 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
}
`,
- },
- {
- name: "invalid national ID - empty",
- request: validators.UpdateReceiverRequest{NationalID: " "},
- want: `
+ },
+ {
+ name: "invalid national ID - empty",
+ request: validators.UpdateReceiverRequest{NationalID: " "},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
@@ -190,11 +184,11 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
}
`,
- },
- {
- name: "invalid national ID - too long",
- request: validators.UpdateReceiverRequest{NationalID: fmt.Sprintf("%0*d", utils.VerificationFieldMaxIdLength+1, 0)},
- want: `
+ },
+ {
+ name: "invalid national ID - too long",
+ request: validators.UpdateReceiverRequest{NationalID: fmt.Sprintf("%0*d", utils.VerificationFieldMaxIdLength+1, 0)},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
@@ -202,11 +196,11 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
}
`,
- },
- {
- name: "invalid email",
- request: validators.UpdateReceiverRequest{Email: "invalid"},
- want: `
+ },
+ {
+ name: "invalid email",
+ request: validators.UpdateReceiverRequest{Email: "invalid"},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
@@ -214,451 +208,421 @@ func Test_UpdateReceiverHandler(t *testing.T) {
}
}
`,
- },
- {
- name: "invalid external ID",
- request: validators.UpdateReceiverRequest{ExternalID: " "},
- want: `
+ },
+ {
+ name: "invalid phone number",
+ request: validators.UpdateReceiverRequest{PhoneNumber: "invalid"},
+ expectedBody: `
{
"error": "request invalid",
"extras": {
- "external_id": "invalid external_id format"
+ "phone_number": "invalid phone number format"
}
}
`,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(tc.request)
- require.NoError(t, err)
- req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
+ },
+ {
+ name: "invalid external ID",
+ request: validators.UpdateReceiverRequest{ExternalID: " "},
+ expectedBody: `
+ {
+ "error": "request invalid",
+ "extras": {
+ "external_id": "external_id cannot be set to empty"
+ }
+ }
+ `,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ route := fmt.Sprintf("/receivers/%s", receiver.ID)
+ reqBody, err := json.Marshal(tc.request)
+ require.NoError(t, err)
+ req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
+ require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
+ rr := httptest.NewRecorder()
+ r.ServeHTTP(rr, req)
- resp := rr.Result()
- respBody, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
+ resp := rr.Result()
+ respBody, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
- assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
- assert.JSONEq(t, tc.want, string(respBody))
- })
- }
- })
+ assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
+ assert.JSONEq(t, tc.expectedBody, string(respBody))
+ })
+ }
+}
+
+func Test_UpdateReceiverHandler_404(t *testing.T) {
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+ dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, err)
+ defer dbConnectionPool.Close()
- t.Run("receiver not found", func(t *testing.T) {
- request := validators.UpdateReceiverRequest{DateOfBirth: "1999-01-01"}
+ models, err := data.NewModels(dbConnectionPool)
+ require.NoError(t, err)
- route := fmt.Sprintf("/receivers/%s", "invalid_receiver_id")
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
- req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
+ handler := &UpdateReceiverHandler{
+ Models: models,
+ DBConnectionPool: dbConnectionPool,
+ }
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
+ // setup
+ r := chi.NewRouter()
+ r.Patch("/receivers/{id}", handler.UpdateReceiver)
- resp := rr.Result()
- assert.Equal(t, http.StatusNotFound, resp.StatusCode)
- })
+ request := validators.UpdateReceiverRequest{DateOfBirth: "1999-01-01"}
- t.Run("update date of birth value", func(t *testing.T) {
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypeDateOfBirth,
- VerificationValue: "2000-01-01",
- })
+ route := fmt.Sprintf("/receivers/%s", "invalid_receiver_id")
+ reqBody, err := json.Marshal(request)
+ require.NoError(t, err)
+ req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
+ require.NoError(t, err)
- request := validators.UpdateReceiverRequest{DateOfBirth: "1999-01-01"}
-
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
- req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
-
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
-
- query := `
- SELECT
- hashed_value
- FROM
- receiver_verifications
- WHERE
- receiver_id = $1 AND
- verification_field = $2
- `
-
- newReceiverVerification := data.ReceiverVerification{}
- err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypeDateOfBirth)
- require.NoError(t, err)
-
- assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "1999-01-01"))
- assert.False(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "2000-01-01"))
-
- receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
- require.NoError(t, err)
- assert.Equal(t, "receiver@email.com", receiverDB.Email)
- assert.Equal(t, "externalID", receiverDB.ExternalID)
- })
+ rr := httptest.NewRecorder()
+ r.ServeHTTP(rr, req)
- t.Run("update year/month value", func(t *testing.T) {
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypeYearMonth,
- VerificationValue: "2000-01",
- })
+ resp := rr.Result()
+ assert.Equal(t, http.StatusNotFound, resp.StatusCode)
+}
- request := validators.UpdateReceiverRequest{YearMonth: "1999-01"}
-
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
- req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
-
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
-
- query := `
- SELECT
- hashed_value
- FROM
- receiver_verifications
- WHERE
- receiver_id = $1 AND
- verification_field = $2
- `
-
- newReceiverVerification := data.ReceiverVerification{}
- err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypeYearMonth)
- require.NoError(t, err)
-
- assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "1999-01"))
- assert.False(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "2000-01"))
-
- receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
- require.NoError(t, err)
- assert.Equal(t, "receiver@email.com", receiverDB.Email)
- assert.Equal(t, "externalID", receiverDB.ExternalID)
- })
+func Test_UpdateReceiverHandler_409(t *testing.T) {
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+ dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, err)
+ defer dbConnectionPool.Close()
- t.Run("update pin value", func(t *testing.T) {
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypePin,
- VerificationValue: "8901",
- })
+ models, err := data.NewModels(dbConnectionPool)
+ require.NoError(t, err)
- request := validators.UpdateReceiverRequest{Pin: "1234"}
-
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
- req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
-
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
-
- query := `
- SELECT
- hashed_value
- FROM
- receiver_verifications
- WHERE
- receiver_id = $1 AND
- verification_field = $2
- `
-
- newReceiverVerification := data.ReceiverVerification{}
- err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypePin)
- require.NoError(t, err)
-
- assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "1234"))
- assert.False(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "8901"))
-
- receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
- require.NoError(t, err)
- assert.Equal(t, "receiver@email.com", receiverDB.Email)
- assert.Equal(t, "externalID", receiverDB.ExternalID)
- })
+ handler := &UpdateReceiverHandler{
+ Models: models,
+ DBConnectionPool: dbConnectionPool,
+ }
- t.Run("update national ID value", func(t *testing.T) {
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypeNationalID,
- VerificationValue: "OLDID890",
- })
+ ctx := context.Background()
- request := validators.UpdateReceiverRequest{NationalID: "NEWID123"}
-
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
- req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
-
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
-
- query := `
- SELECT
- hashed_value
- FROM
- receiver_verifications
- WHERE
- receiver_id = $1 AND
- verification_field = $2
- `
-
- newReceiverVerification := data.ReceiverVerification{}
- err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypeNationalID)
- require.NoError(t, err)
-
- assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "NEWID123"))
- assert.False(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "OLDID890"))
-
- receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
- require.NoError(t, err)
- assert.Equal(t, "receiver@email.com", receiverDB.Email)
- assert.Equal(t, "externalID", receiverDB.ExternalID)
+ // setup
+ r := chi.NewRouter()
+ r.Patch("/receivers/{id}", handler.UpdateReceiver)
+
+ receiverStatic := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{
+ PhoneNumber: "+14155556666",
})
+ receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, nil)
- t.Run("update multiples receiver verifications values", func(t *testing.T) {
- data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool)
+ testCases := []struct {
+ fieldName string
+ request validators.UpdateReceiverRequest
+ expectedBody string
+ }{
+ {
+ fieldName: "email conflict",
+ request: validators.UpdateReceiverRequest{
+ Email: receiverStatic.Email,
+ },
+ expectedBody: `{
+ "error": "The provided email is already associated with another user.",
+ "extras": {
+ "email": "email must be unique"
+ }
+ }`,
+ },
+ {
+ fieldName: "phone_number",
+ request: validators.UpdateReceiverRequest{
+ PhoneNumber: receiverStatic.PhoneNumber,
+ },
+ expectedBody: `{
+ "error": "The provided phone_number is already associated with another user.",
+ "extras": {
+ "phone_number": "phone_number must be unique"
+ }
+ }`,
+ },
+ }
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypeDateOfBirth,
- VerificationValue: "2000-01-01",
- })
+ for _, tc := range testCases {
+ t.Run(tc.fieldName, func(t *testing.T) {
+ route := fmt.Sprintf("/receivers/%s", receiver.ID)
+ reqBody, err := json.Marshal(tc.request)
+ require.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPatch, route, strings.NewReader(string(reqBody)))
+ require.NoError(t, err)
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypeYearMonth,
- VerificationValue: "2000-01",
- })
+ rr := httptest.NewRecorder()
+ r.ServeHTTP(rr, req)
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypePin,
- VerificationValue: "8901",
- })
+ resp := rr.Result()
+ respBody, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
- data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
- ReceiverID: receiver.ID,
- VerificationField: data.VerificationTypeNationalID,
- VerificationValue: "OLDID890",
+ assert.Equal(t, http.StatusConflict, resp.StatusCode)
+ assert.JSONEq(t, tc.expectedBody, string(respBody))
})
+ }
+}
- request := validators.UpdateReceiverRequest{
- DateOfBirth: "1999-01-01",
- YearMonth: "1999-01",
- Pin: "1234",
- NationalID: "NEWID123",
- }
-
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
- req, err := http.NewRequest("PATCH", route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
-
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
-
- query := `
- SELECT
- hashed_value
- FROM
- receiver_verifications
- WHERE
- receiver_id = $1 AND
- verification_field = $2
- `
-
- receiverVerifications := []struct {
- verificationField data.VerificationType
- newVerificationValue string
- oldVerificationValue string
- }{
- {
- verificationField: data.VerificationTypeDateOfBirth,
- newVerificationValue: "1999-01-01",
- oldVerificationValue: "2000-01-01",
- },
- {
- verificationField: data.VerificationTypeYearMonth,
- newVerificationValue: "1999-01",
- oldVerificationValue: "2000-01",
- },
- {
- verificationField: data.VerificationTypePin,
- newVerificationValue: "1234",
- oldVerificationValue: "8901",
- },
- {
- verificationField: data.VerificationTypeNationalID,
- newVerificationValue: "NEWID123",
- oldVerificationValue: "OLDID890",
- },
- }
- for _, v := range receiverVerifications {
- newReceiverVerification := data.ReceiverVerification{}
- err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, v.verificationField)
- require.NoError(t, err)
+func Test_UpdateReceiverHandler_200ok_updateReceiverFields(t *testing.T) {
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+ dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, err)
+ defer dbConnectionPool.Close()
- assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, v.newVerificationValue))
- assert.False(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, v.oldVerificationValue))
+ models, err := data.NewModels(dbConnectionPool)
+ require.NoError(t, err)
- receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
- require.NoError(t, err)
- assert.Equal(t, "receiver@email.com", receiverDB.Email)
- assert.Equal(t, "externalID", receiverDB.ExternalID)
- }
- })
+ handler := &UpdateReceiverHandler{
+ Models: models,
+ DBConnectionPool: dbConnectionPool,
+ }
- t.Run("updates and inserts receiver verifications values", func(t *testing.T) {
- data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool)
+ ctx := context.Background()
- request := validators.UpdateReceiverRequest{
- DateOfBirth: "1999-01-01",
- YearMonth: "1999-01",
- Pin: "1234",
- NationalID: "NEWID123",
- }
+ // setup
+ r := chi.NewRouter()
+ r.Patch("/receivers/{id}", handler.UpdateReceiver)
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
- req, err := http.NewRequest(http.MethodPatch, route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
-
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
-
- query := `
- SELECT
- hashed_value
- FROM
- receiver_verifications
- WHERE
- receiver_id = $1 AND
- verification_field = $2
- `
-
- receiverVerifications := []struct {
- verificationField data.VerificationType
- newVerificationValue string
- oldVerificationValue string
- }{
- {
- verificationField: data.VerificationTypeDateOfBirth,
- newVerificationValue: "1999-01-01",
- oldVerificationValue: "2000-01-01",
+ testCases := []struct {
+ fieldName string
+ request validators.UpdateReceiverRequest
+ assertFn func(t *testing.T, receiver *data.Receiver)
+ }{
+ {
+ fieldName: "email",
+ request: validators.UpdateReceiverRequest{
+ Email: "update_receiver@email.com",
},
- {
- verificationField: data.VerificationTypeYearMonth,
- newVerificationValue: "1999-01",
- oldVerificationValue: "",
+ assertFn: func(t *testing.T, receiver *data.Receiver) {
+ assert.Equal(t, "update_receiver@email.com", receiver.Email)
},
- {
- verificationField: data.VerificationTypePin,
- newVerificationValue: "1234",
- oldVerificationValue: "",
+ },
+ {
+ fieldName: "phone_number",
+ request: validators.UpdateReceiverRequest{
+ PhoneNumber: "+14155556666",
},
- {
- verificationField: data.VerificationTypeNationalID,
- newVerificationValue: "NEWID123",
- oldVerificationValue: "",
+ assertFn: func(t *testing.T, receiver *data.Receiver) {
+ assert.Equal(t, "+14155556666", receiver.PhoneNumber)
},
- }
- for _, v := range receiverVerifications {
- newReceiverVerification := data.ReceiverVerification{}
- err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, v.verificationField)
+ },
+ {
+ fieldName: "external_id",
+ request: validators.UpdateReceiverRequest{
+ ExternalID: "newExternalID",
+ },
+ assertFn: func(t *testing.T, receiver *data.Receiver) {
+ assert.Equal(t, "newExternalID", receiver.ExternalID)
+ },
+ },
+ {
+ fieldName: "ALL FIELDS",
+ request: validators.UpdateReceiverRequest{
+ Email: "update_receiver@email.com",
+ PhoneNumber: "+14155556666",
+ ExternalID: "newExternalID",
+ },
+ assertFn: func(t *testing.T, receiver *data.Receiver) {
+ assert.Equal(t, "update_receiver@email.com", receiver.Email)
+ assert.Equal(t, "+14155556666", receiver.PhoneNumber)
+ assert.Equal(t, "newExternalID", receiver.ExternalID)
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.fieldName, func(t *testing.T) {
+ defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
+ defer data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool)
+
+ receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, nil)
+ data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
+ ReceiverID: receiver.ID,
+ VerificationField: data.VerificationTypeDateOfBirth,
+ VerificationValue: "2000-01-01",
+ })
+
+ route := fmt.Sprintf("/receivers/%s", receiver.ID)
+ reqBody, err := json.Marshal(tc.request)
+ require.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPatch, route, strings.NewReader(string(reqBody)))
require.NoError(t, err)
- t.Logf("newReceiverVerification: %+v", newReceiverVerification)
- assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, v.newVerificationValue))
+ rr := httptest.NewRecorder()
+ r.ServeHTTP(rr, req)
- if v.oldVerificationValue != "" {
- assert.False(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, v.oldVerificationValue))
- }
+ resp := rr.Result()
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
require.NoError(t, err)
- assert.Equal(t, "receiver@email.com", receiverDB.Email)
- assert.Equal(t, "externalID", receiverDB.ExternalID)
- }
- })
- t.Run("updates receiver's email", func(t *testing.T) {
- request := validators.UpdateReceiverRequest{
- Email: "update_receiver@email.com",
- }
+ tc.assertFn(t, receiverDB)
+ })
+ }
+}
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
+// upsertAction is a helper type to define the action to be taken by the handler when upserting the receiver verification.
+type upsertAction string
- req, err := http.NewRequest(http.MethodPatch, route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
+const (
+ actionUpdate upsertAction = "UPDATE"
+ actionInsert upsertAction = "INSERT"
+)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
+// shouldPreInsert is a helper function to determine if the receiver verification should be inserted before the request is
+// made, so we test if the handler is updating the verification value. Otherwise, the receiver verification will be inserted
+// as a consequence of the request.
+func (ua upsertAction) shouldPreInsert() bool {
+ return ua == actionUpdate
+}
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
+func Test_UpdateReceiverHandler_200ok_upsertVerificationFields(t *testing.T) {
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+ dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, err)
+ defer dbConnectionPool.Close()
- receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
- require.NoError(t, err)
- assert.Equal(t, "update_receiver@email.com", receiverDB.Email)
- })
+ models, err := data.NewModels(dbConnectionPool)
+ require.NoError(t, err)
- t.Run("updates receiver's external ID", func(t *testing.T) {
- request := validators.UpdateReceiverRequest{
- ExternalID: "newExternalID",
+ handler := &UpdateReceiverHandler{
+ Models: models,
+ DBConnectionPool: dbConnectionPool,
+ }
+
+ ctx := context.Background()
+
+ // setup
+ r := chi.NewRouter()
+ r.Patch("/receivers/{id}", handler.UpdateReceiver)
+
+ assertVerificationFieldsContains := func(t *testing.T, rvList []data.ReceiverVerification, vt data.VerificationType, verifValue string) {
+ var rv data.ReceiverVerification
+ for _, _rv := range rvList {
+ if _rv.VerificationField == vt {
+ rv = _rv
+ break
+ }
}
+ require.NotEmptyf(t, rv, "receiver verification of type %s not found", vt)
+
+ assert.Equal(t, vt, rv.VerificationField)
+ assert.True(t, data.CompareVerificationValue(rv.HashedValue, verifValue), "hashed value does not match")
+ }
+
+ testCases := []struct {
+ fieldName string
+ request validators.UpdateReceiverRequest
+ assertFn func(t *testing.T, rvList []data.ReceiverVerification)
+ }{
+ {
+ fieldName: "date_of_birth",
+ request: validators.UpdateReceiverRequest{
+ DateOfBirth: "2000-01-01",
+ },
+ assertFn: func(t *testing.T, rvList []data.ReceiverVerification) {
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypeDateOfBirth, "2000-01-01")
+ },
+ },
+ {
+ fieldName: "year_month",
+ request: validators.UpdateReceiverRequest{
+ YearMonth: "2000-01",
+ },
+ assertFn: func(t *testing.T, rvList []data.ReceiverVerification) {
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypeYearMonth, "2000-01")
+ },
+ },
+ {
+ fieldName: "pin",
+ request: validators.UpdateReceiverRequest{
+ Pin: "123456",
+ },
+ assertFn: func(t *testing.T, rvList []data.ReceiverVerification) {
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypePin, "123456")
+ },
+ },
+ {
+ fieldName: "national_id",
+ request: validators.UpdateReceiverRequest{
+ NationalID: "abcd1234",
+ },
+ assertFn: func(t *testing.T, rvList []data.ReceiverVerification) {
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypeNationalID, "abcd1234")
+ },
+ },
+ {
+ fieldName: "ALL FIELDS",
+ request: validators.UpdateReceiverRequest{
+ DateOfBirth: "2000-01-01",
+ YearMonth: "2000-01",
+ Pin: "123456",
+ NationalID: "abcd1234",
+ },
+ assertFn: func(t *testing.T, rvList []data.ReceiverVerification) {
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypeDateOfBirth, "2000-01-01")
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypeYearMonth, "2000-01")
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypePin, "123456")
+ assertVerificationFieldsContains(t, rvList, data.VerificationTypeNationalID, "abcd1234")
+ },
+ },
+ }
- route := fmt.Sprintf("/receivers/%s", receiver.ID)
- reqBody, err := json.Marshal(request)
- require.NoError(t, err)
+ for _, action := range []upsertAction{actionUpdate, actionInsert} {
+ for _, tc := range testCases {
+ t.Run(fmt.Sprintf("%s/%s", action, tc.fieldName), func(t *testing.T) {
+ defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
+ defer data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool)
+
+ receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, nil)
+
+ if action.shouldPreInsert() {
+ data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
+ ReceiverID: receiver.ID,
+ VerificationField: data.VerificationTypeDateOfBirth,
+ VerificationValue: "1999-01-01",
+ })
+ data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
+ ReceiverID: receiver.ID,
+ VerificationField: data.VerificationTypeYearMonth,
+ VerificationValue: "1999-01",
+ })
+ data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
+ ReceiverID: receiver.ID,
+ VerificationField: data.VerificationTypePin,
+ VerificationValue: "000000",
+ })
+ data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{
+ ReceiverID: receiver.ID,
+ VerificationField: data.VerificationTypeNationalID,
+ VerificationValue: "aaaa0000",
+ })
+ }
- req, err := http.NewRequest(http.MethodPatch, route, strings.NewReader(string(reqBody)))
- require.NoError(t, err)
+ route := fmt.Sprintf("/receivers/%s", receiver.ID)
+ reqBody, err := json.Marshal(tc.request)
+ require.NoError(t, err)
+ req, err := http.NewRequest(http.MethodPatch, route, strings.NewReader(string(reqBody)))
+ require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
+ rr := httptest.NewRecorder()
+ r.ServeHTTP(rr, req)
- resp := rr.Result()
- assert.Equal(t, http.StatusOK, resp.StatusCode)
+ resp := rr.Result()
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
- receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID)
- require.NoError(t, err)
+ rvSlice, err := models.ReceiverVerification.GetAllByReceiverId(ctx, dbConnectionPool, receiver.ID)
+ require.NoError(t, err)
- assert.Equal(t, "newExternalID", receiverDB.ExternalID)
- })
+ tc.assertFn(t, rvSlice)
+ })
+ }
+ }
}
diff --git a/internal/serve/validators/receiver_update_validator.go b/internal/serve/validators/receiver_update_validator.go
index b77bf0e42..b68a73397 100644
--- a/internal/serve/validators/receiver_update_validator.go
+++ b/internal/serve/validators/receiver_update_validator.go
@@ -7,11 +7,14 @@ import (
)
type UpdateReceiverRequest struct {
+ // receiver_verifications fields:
DateOfBirth string `json:"date_of_birth"`
YearMonth string `json:"year_month"`
Pin string `json:"pin"`
NationalID string `json:"national_id"`
+ // receivers fields:
Email string `json:"email"`
+ PhoneNumber string `json:"phone_number"`
ExternalID string `json:"external_id"`
}
type UpdateReceiverValidator struct {
@@ -60,8 +63,12 @@ func (ur *UpdateReceiverValidator) ValidateReceiver(updateReceiverRequest *Updat
ur.Check(utils.ValidateEmail(email) == nil, "email", "invalid email format")
}
+ if updateReceiverRequest.PhoneNumber != "" {
+ ur.Check(utils.ValidatePhoneNumber(updateReceiverRequest.PhoneNumber) == nil, "phone_number", "invalid phone number format")
+ }
+
if updateReceiverRequest.ExternalID != "" {
- ur.Check(externalID != "", "external_id", "invalid external_id format")
+ ur.Check(externalID != "", "external_id", "external_id cannot be set to empty")
}
updateReceiverRequest.DateOfBirth = dateOfBirth
diff --git a/internal/serve/validators/receiver_update_validator_test.go b/internal/serve/validators/receiver_update_validator_test.go
index f1960b338..3b7c3a51c 100644
--- a/internal/serve/validators/receiver_update_validator_test.go
+++ b/internal/serve/validators/receiver_update_validator_test.go
@@ -6,100 +6,107 @@ import (
"github.com/stretchr/testify/assert"
)
-func Test_UpdateReceiverValidator_ValidateReceiver(t *testing.T) {
- t.Run("Empty request", func(t *testing.T) {
- validator := NewUpdateReceiverValidator()
-
- receiverInfo := UpdateReceiverRequest{}
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 1, len(validator.Errors))
- assert.Equal(t, "request body is empty", validator.Errors["body"])
- })
-
- t.Run("Invalid date of birth", func(t *testing.T) {
- validator := NewUpdateReceiverValidator()
-
- receiverInfo := UpdateReceiverRequest{
- DateOfBirth: "invalid",
- }
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 1, len(validator.Errors))
- assert.Equal(t, "invalid date of birth format. Correct format: 1990-01-30", validator.Errors["date_of_birth"])
- })
-
- t.Run("Invalid pin", func(t *testing.T) {
- validator := NewUpdateReceiverValidator()
-
- receiverInfo := UpdateReceiverRequest{
- Pin: " ",
- }
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 1, len(validator.Errors))
- assert.Equal(t, "invalid pin length. Cannot have less than 4 or more than 8 characters in pin", validator.Errors["pin"])
- })
-
- t.Run("Invalid national ID", func(t *testing.T) {
- validator := NewUpdateReceiverValidator()
-
- receiverInfo := UpdateReceiverRequest{
- NationalID: " ",
- }
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 1, len(validator.Errors))
- assert.Equal(t, "national id cannot be empty", validator.Errors["national_id"])
- })
-
- t.Run("invalid email", func(t *testing.T) {
- validator := NewUpdateReceiverValidator()
-
- receiverInfo := UpdateReceiverRequest{
- Email: "invalid",
- }
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 1, len(validator.Errors))
- assert.Equal(t, "invalid email format", validator.Errors["email"])
-
- receiverInfo = UpdateReceiverRequest{
- Email: " ",
- }
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 1, len(validator.Errors))
- assert.Equal(t, "invalid email format", validator.Errors["email"])
- })
-
- t.Run("invalid external ID", func(t *testing.T) {
- validator := NewUpdateReceiverValidator()
-
- receiverInfo := UpdateReceiverRequest{
- ExternalID: " ",
- }
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 1, len(validator.Errors))
- assert.Equal(t, "invalid external_id format", validator.Errors["external_id"])
- })
-
- t.Run("Valid receiver values", func(t *testing.T) {
- validator := NewUpdateReceiverValidator()
-
- receiverInfo := UpdateReceiverRequest{
- DateOfBirth: "1999-01-01",
- Pin: "1234 ",
- NationalID: " 12345CODE",
- Email: "receiver@email.com",
- ExternalID: "externalID",
- }
- validator.ValidateReceiver(&receiverInfo)
-
- assert.Equal(t, 0, len(validator.Errors))
- assert.Equal(t, "1999-01-01", receiverInfo.DateOfBirth)
- assert.Equal(t, "1234", receiverInfo.Pin)
- assert.Equal(t, "12345CODE", receiverInfo.NationalID)
- })
+func Test_UpdateReceiverValidator_ValidateReceiver2(t *testing.T) {
+ testCases := []struct {
+ name string
+ request UpdateReceiverRequest
+ expectedErrors map[string]interface{}
+ }{
+ {
+ name: "Empty request",
+ request: UpdateReceiverRequest{},
+ expectedErrors: map[string]interface{}{
+ "body": "request body is empty",
+ },
+ },
+ {
+ name: "[DATE_OF_BIRTH] ValidationField is invalid",
+ request: UpdateReceiverRequest{
+ DateOfBirth: "invalid",
+ },
+ expectedErrors: map[string]interface{}{
+ "date_of_birth": "invalid date of birth format. Correct format: 1990-01-30",
+ },
+ },
+ {
+ name: "[YEAR_MONTH] ValidationField is invalid",
+ request: UpdateReceiverRequest{
+ YearMonth: "invalid",
+ },
+ expectedErrors: map[string]interface{}{
+ "year_month": "invalid year/month format. Correct format: 1990-12",
+ },
+ },
+ {
+ name: "[PIN] ValidationField is invalid",
+ request: UpdateReceiverRequest{
+ Pin: " ",
+ },
+ expectedErrors: map[string]interface{}{
+ "pin": "invalid pin length. Cannot have less than 4 or more than 8 characters in pin",
+ },
+ },
+ {
+ name: "[NATIONAL_ID_NUMBER] ValidationField is invalid",
+ request: UpdateReceiverRequest{
+ NationalID: " ",
+ },
+ expectedErrors: map[string]interface{}{
+ "national_id": "national id cannot be empty",
+ },
+ },
+ {
+ name: "e-mail is invalid",
+ request: UpdateReceiverRequest{
+ Email: "invalid",
+ },
+ expectedErrors: map[string]interface{}{
+ "email": "invalid email format",
+ },
+ },
+ {
+ name: "phone number is invalid",
+ request: UpdateReceiverRequest{
+ PhoneNumber: "invalid",
+ },
+ expectedErrors: map[string]interface{}{
+ "phone_number": "invalid phone number format",
+ },
+ },
+ {
+ name: "external ID is invalid",
+ request: UpdateReceiverRequest{
+ ExternalID: " ",
+ },
+ expectedErrors: map[string]interface{}{
+ "external_id": "external_id cannot be set to empty",
+ },
+ },
+ {
+ name: "๐ Valid receiver values",
+ request: UpdateReceiverRequest{
+ DateOfBirth: "1999-01-01",
+ YearMonth: "1999-01",
+ Pin: "1234 ",
+ NationalID: " 12345CODE",
+ Email: "receiver@email.com",
+ PhoneNumber: "+14155556666",
+ ExternalID: "externalID",
+ },
+ expectedErrors: map[string]interface{}{},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ validator := NewUpdateReceiverValidator()
+ validator.ValidateReceiver(&tc.request)
+
+ assert.Equal(t, len(tc.expectedErrors), len(validator.Errors))
+ assert.Equal(t, tc.expectedErrors, validator.Errors)
+ for key, value := range tc.expectedErrors {
+ assert.Equal(t, value, validator.Errors[key])
+ }
+ })
+ }
}
From 2f5adf5d2574806ef4b6c390e7f3d0f0ee5571ff Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 22 Oct 2024 10:15:14 -0700
Subject: [PATCH 40/75] [SDP-1355] increase token refresh window (#437)
### What
Increase token refresh window.
### Why
It was too short and causing the session to expire very often. We're changing it with this frontend change, where we add a token refresher to the frontend codebase.
---
stellar-auth/pkg/auth/auth.go | 2 ++
stellar-auth/pkg/auth/jwt_manager.go | 13 +++++++++----
stellar-auth/pkg/auth/jwt_manager_test.go | 4 ++--
3 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/stellar-auth/pkg/auth/auth.go b/stellar-auth/pkg/auth/auth.go
index cbf46312c..206abd0c7 100644
--- a/stellar-auth/pkg/auth/auth.go
+++ b/stellar-auth/pkg/auth/auth.go
@@ -11,6 +11,8 @@ var ErrInvalidToken = errors.New("invalid token")
type AuthManager interface {
Authenticate(ctx context.Context, email, pass string) (string, error)
+ // RefreshToken generates a new token if the current token is going to expire in less than `tokenRefreshWindow` minutes.
+ // Otherwise, it returns the same token.
RefreshToken(ctx context.Context, tokenString string) (string, error)
ValidateToken(ctx context.Context, tokenString string) (bool, error)
AllRolesInTokenUser(ctx context.Context, tokenString string, roleNames []string) (bool, error)
diff --git a/stellar-auth/pkg/auth/jwt_manager.go b/stellar-auth/pkg/auth/jwt_manager.go
index 25ed7f566..7557930a3 100644
--- a/stellar-auth/pkg/auth/jwt_manager.go
+++ b/stellar-auth/pkg/auth/jwt_manager.go
@@ -6,17 +6,20 @@ import (
"fmt"
"time"
+ jwtgo "github.com/golang-jwt/jwt/v4"
"github.com/stellar/go/support/log"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant"
-
- jwtgo "github.com/golang-jwt/jwt/v4"
)
-const defaultRefreshTimeoutInMinutes = 2
+// tokenRefreshWindow is the time window in minutes that we allow to refresh a token. If the token is going to expire in
+// less than this time, we generate a new token, otherwise we return the same token.
+const tokenRefreshWindow = 3
type JWTManager interface {
GenerateToken(ctx context.Context, user *User, expiresAt time.Time) (string, error)
+ // RefreshToken generates a new token if the current token is going to expire in less than `tokenRefreshWindow` minutes.
+ // Otherwise, it returns the same token.
RefreshToken(ctx context.Context, token string, expiresAt time.Time) (string, error)
ValidateToken(ctx context.Context, token string) (bool, error)
GetUserFromToken(ctx context.Context, token string) (*User, error)
@@ -92,6 +95,8 @@ func (m *defaultJWTManager) GenerateToken(ctx context.Context, user *User, expir
return tokenString, nil
}
+// RefreshToken generates a new token if the current token is going to expire in less than `tokenRefreshWindow` minutes.
+// Otherwise, it returns the same token.
func (m *defaultJWTManager) RefreshToken(ctx context.Context, tokenString string, expiresAt time.Time) (string, error) {
_, c, err := m.parseToken(tokenString)
if err != nil {
@@ -100,7 +105,7 @@ func (m *defaultJWTManager) RefreshToken(ctx context.Context, tokenString string
// We only generate new tokens when enough time
// is elapsed.
- if time.Until(c.ExpiresAt.Time) > defaultRefreshTimeoutInMinutes*time.Minute {
+ if time.Until(c.ExpiresAt.Time) > tokenRefreshWindow*time.Minute {
return tokenString, nil
}
diff --git a/stellar-auth/pkg/auth/jwt_manager_test.go b/stellar-auth/pkg/auth/jwt_manager_test.go
index 1f0ef7ae9..453af509b 100644
--- a/stellar-auth/pkg/auth/jwt_manager_test.go
+++ b/stellar-auth/pkg/auth/jwt_manager_test.go
@@ -117,7 +117,7 @@ func Test_DefaultJWTManager_RefreshToken(t *testing.T) {
ctx := tenant.SaveTenantInContext(context.Background(), ¤tTenant)
t.Run("returns the same token when is above the refresh period", func(t *testing.T) {
- expiresAt := time.Now().Add(time.Minute * (defaultRefreshTimeoutInMinutes + 1))
+ expiresAt := time.Now().Add(time.Minute * (tokenRefreshWindow + 1))
token, err := jwtManager.GenerateToken(ctx, &User{}, expiresAt)
require.NoError(t, err)
@@ -129,7 +129,7 @@ func Test_DefaultJWTManager_RefreshToken(t *testing.T) {
})
t.Run("returns a refreshed token", func(t *testing.T) {
- expiresAt := time.Now().Add(time.Minute * defaultRefreshTimeoutInMinutes)
+ expiresAt := time.Now().Add(time.Minute * tokenRefreshWindow)
token, err := jwtManager.GenerateToken(ctx, &User{}, expiresAt)
require.NoError(t, err)
From 41bf29a39efcb6e679bcd7358ac8bf8f530873a4 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 22 Oct 2024 10:34:47 -0700
Subject: [PATCH 41/75] [SDP-1335] Update `DELETE .../phone-number/...` to
`DELETE .../contact-info/...` (#438)
### What
Update `DELETE .../phone-number/...` to `DELETE .../contact-info/...`
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1335
---
internal/data/fixtures.go | 2 +-
internal/data/receivers.go | 25 +-
internal/data/receivers_test.go | 366 +++++++++---------
...dler.go => delete_contact_info_handler.go} | 25 +-
.../delete_contact_info_handler_test.go | 104 +++++
.../delete_phone_number_handler_test.go | 103 -----
internal/serve/serve.go | 2 +-
7 files changed, 326 insertions(+), 301 deletions(-)
rename internal/serve/httphandler/{delete_phone_number_handler.go => delete_contact_info_handler.go} (61%)
create mode 100644 internal/serve/httphandler/delete_contact_info_handler_test.go
delete mode 100644 internal/serve/httphandler/delete_phone_number_handler_test.go
diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go
index 902c53f22..437ef291b 100644
--- a/internal/data/fixtures.go
+++ b/internal/data/fixtures.go
@@ -289,7 +289,7 @@ func ClearAndCreateCountryFixtures(t *testing.T, ctx context.Context, sqlExec db
func CreateReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, r *Receiver) *Receiver {
t.Helper()
- randomSuffix, err := utils.RandomString(5)
+ randomSuffix, err := utils.RandomString(5, utils.NumberBytes)
require.NoError(t, err)
if r == nil {
diff --git a/internal/data/receivers.go b/internal/data/receivers.go
index 877a237b0..3cdac40b0 100644
--- a/internal/data/receivers.go
+++ b/internal/data/receivers.go
@@ -119,14 +119,14 @@ type ReceivedAmounts []Amount
func (ra *ReceivedAmounts) Scan(src interface{}) error {
var receivedAmounts sql.NullString
if err := (&receivedAmounts).Scan(src); err != nil {
- return fmt.Errorf("error scanning status history value: %w", err)
+ return fmt.Errorf("scanning status history value: %w", err)
}
if receivedAmounts.Valid {
var shEntry []Amount
err := json.Unmarshal([]byte(receivedAmounts.String), &shEntry)
if err != nil {
- return fmt.Errorf("error unmarshaling status_history column: %w", err)
+ return fmt.Errorf("unmarshaling status_history column: %w", err)
}
*ra = shEntry
@@ -209,7 +209,7 @@ func (r *ReceiverModel) Get(ctx context.Context, sqlExec db.SQLExecuter, id stri
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrRecordNotFound
} else {
- return nil, fmt.Errorf("error querying receiver ID: %w", err)
+ return nil, fmt.Errorf("querying receiver ID: %w", err)
}
}
@@ -229,7 +229,7 @@ func (r *ReceiverModel) Count(ctx context.Context, sqlExec db.SQLExecuter, query
err := sqlExec.GetContext(ctx, &count, query, params...)
if err != nil {
- return 0, fmt.Errorf("error counting payments: %w", err)
+ return 0, fmt.Errorf("counting payments: %w", err)
}
return count, nil
@@ -313,7 +313,7 @@ func (r *ReceiverModel) GetAll(ctx context.Context, sqlExec db.SQLExecuter, quer
err := sqlExec.SelectContext(ctx, &receivers, query, params...)
if err != nil {
- return nil, fmt.Errorf("error querying receivers: %w", err)
+ return nil, fmt.Errorf("querying receivers: %w", err)
}
return receivers, nil
@@ -460,19 +460,20 @@ func (r *ReceiverModel) GetByContacts(ctx context.Context, sqlExec db.SQLExecute
return receivers, nil
}
-// DeleteByPhoneNumber deletes a receiver by phone number. It also deletes the associated entries in other tables:
-// messages, payments, receiver_verifications, receiver_wallets, receivers, disbursements, submitter_transactions
-func (r *ReceiverModel) DeleteByPhoneNumber(ctx context.Context, dbConnectionPool db.DBConnectionPool, phoneNumber string) error {
+// DeleteByContactInfo deletes a receiver by phone number or email. It also deletes the associated entries in other
+// tables: messages, payments, receiver_verifications, receiver_wallets, receivers, disbursements,
+// submitter_transactions.
+func (r *ReceiverModel) DeleteByContactInfo(ctx context.Context, dbConnectionPool db.DBConnectionPool, contactInfo string) error {
return db.RunInTransaction(ctx, dbConnectionPool, nil, func(dbTx db.DBTransaction) error {
- query := "SELECT id FROM receivers WHERE phone_number = $1"
+ query := "SELECT id FROM receivers WHERE phone_number = $1 OR email = $1"
var receiverID string
- err := dbTx.GetContext(ctx, &receiverID, query, phoneNumber)
+ err := dbTx.GetContext(ctx, &receiverID, query, contactInfo)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ErrRecordNotFound
}
- return fmt.Errorf("error fetching receiver by phone number %s: %w", phoneNumber, err)
+ return fmt.Errorf("fetching receiver by contact info %s: %w", contactInfo, err)
}
type QueryWithParams struct {
@@ -493,7 +494,7 @@ func (r *ReceiverModel) DeleteByPhoneNumber(ctx context.Context, dbConnectionPoo
for _, qwp := range queries {
_, err = dbTx.ExecContext(ctx, qwp.Query, qwp.Params...)
if err != nil {
- return fmt.Errorf("error executing query %q: %w", qwp.Query, err)
+ return fmt.Errorf("executing query %q: %w", qwp.Query, err)
}
}
diff --git a/internal/data/receivers_test.go b/internal/data/receivers_test.go
index d391173bf..098ad3ec2 100644
--- a/internal/data/receivers_test.go
+++ b/internal/data/receivers_test.go
@@ -901,7 +901,7 @@ func Test_ReceiversModel_ParseReceiverIDs(t *testing.T) {
require.NoError(t, err)
}
-func Test_DeleteByPhoneNumber(t *testing.T) {
+func Test_DeleteByContactInfo(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
@@ -912,186 +912,198 @@ func Test_DeleteByPhoneNumber(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
- // 0. returns ErrNotFound for users that don't exist:
- t.Run("User does not exist", func(t *testing.T) {
- err = models.Receiver.DeleteByPhoneNumber(ctx, dbConnectionPool, "+14152222222")
- require.ErrorIs(t, err, ErrRecordNotFound)
- })
-
- // 1. Create country, asset, and wallet (won't be deleted)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "ATL", "Atlantis")
- asset := CreateAssetFixture(t, ctx, dbConnectionPool, "FOO1", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "walletA", "https://www.a.com", "www.a.com", "a://")
-
- // 2. Create receiverX (that will be deleted) and all receiverX dependent resources that will also be deleted:
- receiverX := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
- receiverWalletX := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverX.ID, wallet.ID, DraftReceiversWalletStatus)
- _ = CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{
- ReceiverID: receiverX.ID,
- VerificationField: VerificationTypeDateOfBirth,
- VerificationValue: "1990-01-01",
- })
- messageX := CreateMessageFixture(t, ctx, dbConnectionPool, &Message{
- Type: message.MessengerTypeTwilioSMS,
- AssetID: nil,
- ReceiverID: receiverX.ID,
- WalletID: wallet.ID,
- ReceiverWalletID: &receiverWalletX.ID,
- Status: SuccessMessageStatus,
- CreatedAt: time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC),
- })
- disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
- })
- paymentX1 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
- ReceiverWallet: receiverWalletX,
- Disbursement: disbursement1,
- Asset: *asset,
- Status: ReadyPaymentStatus,
- Amount: "1",
- })
-
- // 3. Create receiverY (that will not be deleted) and all receiverY dependent resources that will not be deleted:
- receiverY := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
- receiverWalletY := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverY.ID, wallet.ID, DraftReceiversWalletStatus)
- _ = CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{
- ReceiverID: receiverY.ID,
- VerificationField: VerificationTypeDateOfBirth,
- VerificationValue: "1990-01-01",
- })
- messageY := CreateMessageFixture(t, ctx, dbConnectionPool, &Message{
- Type: message.MessengerTypeTwilioSMS,
- AssetID: nil,
- ReceiverID: receiverY.ID,
- WalletID: wallet.ID,
- ReceiverWalletID: &receiverWalletY.ID,
- Status: SuccessMessageStatus,
- CreatedAt: time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC),
- })
- disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
- })
- paymentY2 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
- ReceiverWallet: receiverWalletY,
- Disbursement: disbursement2,
- Asset: *asset,
- Status: ReadyPaymentStatus,
- Amount: "1",
- })
-
- paymentX2 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
- ReceiverWallet: receiverWalletX,
- Disbursement: disbursement2,
- Asset: *asset,
- Status: ReadyPaymentStatus,
- Amount: "1",
- }) // This payment will be deleted along with the remaining receiverX-related data
-
- // 4. Delete receiverX
- err = models.Receiver.DeleteByPhoneNumber(ctx, dbConnectionPool, receiverX.PhoneNumber)
- require.NoError(t, err)
+ for _, contactType := range GetAllReceiverContactTypes() {
+ t.Run(string(contactType), func(t *testing.T) {
+ defer func() {
+ err = db.RunInTransaction(ctx, dbConnectionPool, nil, func(dbTx db.DBTransaction) error {
+ DeleteAllFixtures(t, ctx, dbTx)
+ return nil
+ })
+ require.NoError(t, err)
+ }()
+
+ // 0. returns ErrNotFound for users that don't exist:
+ t.Run("User does not exist", func(t *testing.T) {
+ err = models.Receiver.DeleteByContactInfo(ctx, dbConnectionPool, "+14152222222")
+ require.ErrorIs(t, err, ErrRecordNotFound)
+ })
+
+ // 1. Create country, asset, and wallet (won't be deleted)
+ country := CreateCountryFixture(t, ctx, dbConnectionPool, "ATL", "Atlantis")
+ asset := CreateAssetFixture(t, ctx, dbConnectionPool, "FOO1", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
+ wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "walletA", "https://www.a.com", "www.a.com", "a://")
+
+ // 2. Create receiverX (that will be deleted) and all receiverX dependent resources that will also be deleted:
+ receiverX := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
+ receiverWalletX := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverX.ID, wallet.ID, DraftReceiversWalletStatus)
+ _ = CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{
+ ReceiverID: receiverX.ID,
+ VerificationField: VerificationTypeDateOfBirth,
+ VerificationValue: "1990-01-01",
+ })
+ messageX := CreateMessageFixture(t, ctx, dbConnectionPool, &Message{
+ Type: message.MessengerTypeTwilioSMS,
+ AssetID: nil,
+ ReceiverID: receiverX.ID,
+ WalletID: wallet.ID,
+ ReceiverWalletID: &receiverWalletX.ID,
+ Status: SuccessMessageStatus,
+ CreatedAt: time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC),
+ })
+ disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
+ Country: country,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
+ })
+ paymentX1 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
+ ReceiverWallet: receiverWalletX,
+ Disbursement: disbursement1,
+ Asset: *asset,
+ Status: ReadyPaymentStatus,
+ Amount: "1",
+ })
+
+ // 3. Create receiverY (that will not be deleted) and all receiverY dependent resources that will not be deleted:
+ receiverY := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
+ receiverWalletY := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverY.ID, wallet.ID, DraftReceiversWalletStatus)
+ _ = CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{
+ ReceiverID: receiverY.ID,
+ VerificationField: VerificationTypeDateOfBirth,
+ VerificationValue: "1990-01-01",
+ })
+ messageY := CreateMessageFixture(t, ctx, dbConnectionPool, &Message{
+ Type: message.MessengerTypeTwilioSMS,
+ AssetID: nil,
+ ReceiverID: receiverY.ID,
+ WalletID: wallet.ID,
+ ReceiverWalletID: &receiverWalletY.ID,
+ Status: SuccessMessageStatus,
+ CreatedAt: time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC),
+ })
+ disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
+ Country: country,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
+ })
+ paymentY2 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
+ ReceiverWallet: receiverWalletY,
+ Disbursement: disbursement2,
+ Asset: *asset,
+ Status: ReadyPaymentStatus,
+ Amount: "1",
+ })
+
+ paymentX2 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
+ ReceiverWallet: receiverWalletX,
+ Disbursement: disbursement2,
+ Asset: *asset,
+ Status: ReadyPaymentStatus,
+ Amount: "1",
+ }) // This payment will be deleted along with the remaining receiverX-related data
+
+ // 4. Delete receiverX
+ err = models.Receiver.DeleteByContactInfo(ctx, dbConnectionPool, receiverX.ContactByType(contactType))
+ require.NoError(t, err)
- type testCase struct {
- name string
- query string
- args []interface{}
- wantExists bool
- }
+ type testCase struct {
+ name string
+ query string
+ args []interface{}
+ wantExists bool
+ }
- // 5. Prepare assertions to make sure `DeleteByPhoneNumber` DID DELETE receiverX-related data:
- didDeleteTestCases := []testCase{
- {
- name: "DID DELETE: receiverX",
- query: "SELECT EXISTS(SELECT 1 FROM receivers WHERE id = $1)",
- args: []interface{}{receiverX.ID},
- wantExists: false,
- },
- {
- name: "DID DELETE: receiverWalletX",
- query: "SELECT EXISTS(SELECT 1 FROM receiver_wallets WHERE id = $1)",
- args: []interface{}{receiverWalletX.ID},
- wantExists: false,
- },
- {
- name: "DID DELETE: receiverVerificationX",
- query: "SELECT EXISTS(SELECT 1 FROM receiver_verifications WHERE receiver_id = $1)",
- args: []interface{}{receiverX.ID},
- wantExists: false,
- },
- {
- name: "DID DELETE: messageX",
- query: "SELECT EXISTS(SELECT 1 FROM messages WHERE id = $1)",
- args: []interface{}{messageX.ID},
- wantExists: false,
- },
- {
- name: "DID DELETE: paymentX",
- query: "SELECT EXISTS(SELECT 1 FROM payments WHERE id = ANY($1))",
- args: []interface{}{pq.Array([]string{paymentX1.ID, paymentX2.ID})},
- wantExists: false,
- },
- {
- name: "DID DELETE: disbursement1",
- query: "SELECT EXISTS(SELECT 1 FROM disbursements WHERE id = $1)",
- args: []interface{}{disbursement1.ID},
- wantExists: false,
- },
- }
+ // 5. Prepare assertions to make sure `DeleteByContactInfo` DID DELETE receiverX-related data:
+ didDeleteTestCases := []testCase{
+ {
+ name: "DID DELETE: receiverX",
+ query: "SELECT EXISTS(SELECT 1 FROM receivers WHERE id = $1)",
+ args: []interface{}{receiverX.ID},
+ wantExists: false,
+ },
+ {
+ name: "DID DELETE: receiverWalletX",
+ query: "SELECT EXISTS(SELECT 1 FROM receiver_wallets WHERE id = $1)",
+ args: []interface{}{receiverWalletX.ID},
+ wantExists: false,
+ },
+ {
+ name: "DID DELETE: receiverVerificationX",
+ query: "SELECT EXISTS(SELECT 1 FROM receiver_verifications WHERE receiver_id = $1)",
+ args: []interface{}{receiverX.ID},
+ wantExists: false,
+ },
+ {
+ name: "DID DELETE: messageX",
+ query: "SELECT EXISTS(SELECT 1 FROM messages WHERE id = $1)",
+ args: []interface{}{messageX.ID},
+ wantExists: false,
+ },
+ {
+ name: "DID DELETE: paymentX",
+ query: "SELECT EXISTS(SELECT 1 FROM payments WHERE id = ANY($1))",
+ args: []interface{}{pq.Array([]string{paymentX1.ID, paymentX2.ID})},
+ wantExists: false,
+ },
+ {
+ name: "DID DELETE: disbursement1",
+ query: "SELECT EXISTS(SELECT 1 FROM disbursements WHERE id = $1)",
+ args: []interface{}{disbursement1.ID},
+ wantExists: false,
+ },
+ }
- // 6. Prepare assertions to make sure `DeleteByPhoneNumber` DID NOT DELETE receiverY-related data:
- didNotDeleteTestCases := []testCase{
- {
- name: "DID NOT DELETE: receiverY",
- query: "SELECT EXISTS(SELECT 1 FROM receivers WHERE id = $1)",
- args: []interface{}{receiverY.ID},
- wantExists: true,
- },
- {
- name: "DID NOT DELETE: receiverWalletY",
- query: "SELECT EXISTS(SELECT 1 FROM receiver_wallets WHERE id = $1)",
- args: []interface{}{receiverWalletY.ID},
- wantExists: true,
- },
- {
- name: "DID NOT DELETE: receiverVerificationY",
- query: "SELECT EXISTS(SELECT 1 FROM receiver_verifications WHERE receiver_id = $1)",
- args: []interface{}{receiverY.ID},
- wantExists: true,
- },
- {
- name: "DID NOT DELETE: messageY",
- query: "SELECT EXISTS(SELECT 1 FROM messages WHERE id = $1)",
- args: []interface{}{messageY.ID},
- wantExists: true,
- },
- {
- name: "DID NOT DELETE: paymentY2",
- query: "SELECT EXISTS(SELECT 1 FROM payments WHERE id = $1)",
- args: []interface{}{paymentY2.ID},
- wantExists: true,
- },
- {
- name: "DID NOT DELETE: paymentX2",
- query: "SELECT EXISTS(SELECT 1 FROM disbursements WHERE id = $1)",
- args: []interface{}{disbursement2.ID},
- wantExists: true,
- },
- }
+ // 6. Prepare assertions to make sure `DeleteByContactInfo` DID NOT DELETE receiverY-related data:
+ didNotDeleteTestCases := []testCase{
+ {
+ name: "DID NOT DELETE: receiverY",
+ query: "SELECT EXISTS(SELECT 1 FROM receivers WHERE id = $1)",
+ args: []interface{}{receiverY.ID},
+ wantExists: true,
+ },
+ {
+ name: "DID NOT DELETE: receiverWalletY",
+ query: "SELECT EXISTS(SELECT 1 FROM receiver_wallets WHERE id = $1)",
+ args: []interface{}{receiverWalletY.ID},
+ wantExists: true,
+ },
+ {
+ name: "DID NOT DELETE: receiverVerificationY",
+ query: "SELECT EXISTS(SELECT 1 FROM receiver_verifications WHERE receiver_id = $1)",
+ args: []interface{}{receiverY.ID},
+ wantExists: true,
+ },
+ {
+ name: "DID NOT DELETE: messageY",
+ query: "SELECT EXISTS(SELECT 1 FROM messages WHERE id = $1)",
+ args: []interface{}{messageY.ID},
+ wantExists: true,
+ },
+ {
+ name: "DID NOT DELETE: paymentY2",
+ query: "SELECT EXISTS(SELECT 1 FROM payments WHERE id = $1)",
+ args: []interface{}{paymentY2.ID},
+ wantExists: true,
+ },
+ {
+ name: "DID NOT DELETE: paymentX2",
+ query: "SELECT EXISTS(SELECT 1 FROM disbursements WHERE id = $1)",
+ args: []interface{}{disbursement2.ID},
+ wantExists: true,
+ },
+ }
- // 7. Run assertions
- testCases := append(didDeleteTestCases, didNotDeleteTestCases...)
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- var exists bool
- err = dbConnectionPool.QueryRowxContext(ctx, tc.query, tc.args...).Scan(&exists)
- require.NoError(t, err)
- require.Equal(t, tc.wantExists, exists)
+ // 7. Run assertions
+ testCases := append(didDeleteTestCases, didNotDeleteTestCases...)
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var exists bool
+ err = dbConnectionPool.QueryRowxContext(ctx, tc.query, tc.args...).Scan(&exists)
+ require.NoError(t, err)
+ require.Equal(t, tc.wantExists, exists)
+ })
+ }
})
}
}
diff --git a/internal/serve/httphandler/delete_phone_number_handler.go b/internal/serve/httphandler/delete_contact_info_handler.go
similarity index 61%
rename from internal/serve/httphandler/delete_phone_number_handler.go
rename to internal/serve/httphandler/delete_contact_info_handler.go
index 6a6ead241..f90d0aeaa 100644
--- a/internal/serve/httphandler/delete_phone_number_handler.go
+++ b/internal/serve/httphandler/delete_contact_info_handler.go
@@ -3,6 +3,7 @@ package httphandler
import (
"errors"
"net/http"
+ "strings"
"github.com/go-chi/chi/v5"
"github.com/stellar/go/network"
@@ -14,27 +15,37 @@ import (
"github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
-type DeletePhoneNumberHandler struct {
+type DeleteContactInfoHandler struct {
NetworkPassphrase string
Models *data.Models
}
-func (d DeletePhoneNumberHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (d DeleteContactInfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if d.NetworkPassphrase != network.TestNetworkPassphrase {
httperror.NotFound("", nil, nil).Render(w)
return
}
- phoneNumber := chi.URLParam(r, "phone_number")
- if err := utils.ValidatePhoneNumber(phoneNumber); err != nil {
- extras := map[string]interface{}{"phone_number": "invalid phone number"}
+ contactInfo := strings.TrimSpace(chi.URLParam(r, "contact_info"))
+ extras := map[string]interface{}{}
+ if contactInfo == "" {
+ extras["contact_info"] = "contact_info is required"
+ } else {
+ phoneNumberErr := utils.ValidatePhoneNumber(contactInfo)
+ emailErr := utils.ValidateEmail(contactInfo)
+
+ if phoneNumberErr != nil && emailErr != nil {
+ extras["contact_info"] = "not a valid phone number or email"
+ }
+ }
+ if len(extras) > 0 {
httperror.BadRequest("", nil, extras).Render(w)
return
}
- log.Ctx(ctx).Warnf("Deleting user with phone number %s", utils.TruncateString(phoneNumber, 3))
- err := d.Models.Receiver.DeleteByPhoneNumber(ctx, d.Models.DBConnectionPool, phoneNumber)
+ log.Ctx(ctx).Warnf("Deleting user with phone number %s", utils.TruncateString(contactInfo, 3))
+ err := d.Models.Receiver.DeleteByContactInfo(ctx, d.Models.DBConnectionPool, contactInfo)
if err != nil {
if errors.Is(err, data.ErrRecordNotFound) {
httperror.NotFound("", err, nil).Render(w)
diff --git a/internal/serve/httphandler/delete_contact_info_handler_test.go b/internal/serve/httphandler/delete_contact_info_handler_test.go
new file mode 100644
index 000000000..e1fa91ceb
--- /dev/null
+++ b/internal/serve/httphandler/delete_contact_info_handler_test.go
@@ -0,0 +1,104 @@
+package httphandler
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/stellar/go/network"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/stellar/stellar-disbursement-platform-backend/db"
+ "github.com/stellar/stellar-disbursement-platform-backend/db/dbtest"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
+)
+
+func Test_DeleteContactInfoHandler(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()
+ models, err := data.NewModels(dbConnectionPool)
+ require.NoError(t, err)
+
+ for _, contactType := range data.GetAllReceiverContactTypes() {
+ t.Run(string(contactType), func(t *testing.T) {
+ testCases := []struct {
+ name string
+ networkPassphrase string
+ getContactinfoFn func(t *testing.T, receiver *data.Receiver) string
+ wantStatusCode int
+ wantBody string
+ }{
+ {
+ name: "๐ด return 404 if network passphrase is not testnet",
+ networkPassphrase: network.PublicNetworkPassphrase,
+ getContactinfoFn: func(t *testing.T, receiver *data.Receiver) string { return receiver.ContactByType(contactType) },
+ wantStatusCode: http.StatusNotFound,
+ wantBody: `{"error": "Resource not found."}`,
+ },
+ {
+ name: "๐ด return 400 if the contact info is invalid",
+ networkPassphrase: network.TestNetworkPassphrase,
+ getContactinfoFn: func(t *testing.T, receiver *data.Receiver) string { return "foobar" },
+ wantStatusCode: http.StatusBadRequest,
+ wantBody: `{
+ "error": "The request was invalid in some way.",
+ "extras": {
+ "contact_info": "not a valid phone number or email"
+ }
+ }`,
+ },
+ {
+ name: "๐ด return 404 if the contact info does not exist",
+ networkPassphrase: network.TestNetworkPassphrase,
+ getContactinfoFn: func(t *testing.T, receiver *data.Receiver) string {
+ switch contactType {
+ case data.ReceiverContactTypeEmail:
+ return "foobar@test.com"
+ case data.ReceiverContactTypeSMS:
+ return "+14153333333"
+ }
+ t.Errorf("Unsupported contact type %s", contactType)
+ panic("Unsupported contact type " + contactType)
+ },
+ wantStatusCode: http.StatusNotFound,
+ wantBody: `{"error":"Resource not found."}`,
+ },
+ {
+ name: "๐ข return 204 if the contact info exists",
+ networkPassphrase: network.TestNetworkPassphrase,
+ getContactinfoFn: func(t *testing.T, receiver *data.Receiver) string { return receiver.ContactByType(contactType) },
+ wantStatusCode: http.StatusNoContent,
+ wantBody: "null",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
+
+ h := DeleteContactInfoHandler{NetworkPassphrase: tc.networkPassphrase, Models: models}
+ r := chi.NewRouter()
+ r.Delete("/wallet-registration/contact-info/{contact_info}", h.ServeHTTP)
+
+ // test
+ req, err := http.NewRequest("DELETE", "/wallet-registration/contact-info/"+tc.getContactinfoFn(t, receiver), nil)
+ require.NoError(t, err)
+ rr := httptest.NewRecorder()
+ r.ServeHTTP(rr, req)
+
+ // assert response
+ assert.Equal(t, tc.wantStatusCode, rr.Code)
+ assert.JSONEq(t, tc.wantBody, rr.Body.String())
+ })
+ }
+ })
+ }
+}
diff --git a/internal/serve/httphandler/delete_phone_number_handler_test.go b/internal/serve/httphandler/delete_phone_number_handler_test.go
deleted file mode 100644
index 48d7eba94..000000000
--- a/internal/serve/httphandler/delete_phone_number_handler_test.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package httphandler
-
-import (
- "context"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/go-chi/chi/v5"
- "github.com/stellar/go/network"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/stellar/stellar-disbursement-platform-backend/db"
- "github.com/stellar/stellar-disbursement-platform-backend/db/dbtest"
- "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
-)
-
-func Test_DeletePhoneNumberHandler(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()
- models, err := data.NewModels(dbConnectionPool)
- require.NoError(t, err)
- receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: "+14152222222"})
-
- t.Run("return 404 if network passphrase is not testnet", func(t *testing.T) {
- h := DeletePhoneNumberHandler{NetworkPassphrase: network.PublicNetworkPassphrase, Models: models}
- r := chi.NewRouter()
- r.Delete("/wallet-registration/phone-number/{phone_number}", h.ServeHTTP)
-
- // test
- req, err := http.NewRequest("DELETE", "/wallet-registration/phone-number/"+receiver.PhoneNumber, nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- // assert response
- assert.Equal(t, http.StatusNotFound, rr.Code)
- wantJson := `{"error": "Resource not found."}`
- assert.JSONEq(t, wantJson, rr.Body.String())
- })
-
- t.Run("return 400 if network passphrase is testnet but phone number is invalid", func(t *testing.T) {
- h := DeletePhoneNumberHandler{NetworkPassphrase: network.TestNetworkPassphrase, Models: models}
- r := chi.NewRouter()
- r.Delete("/wallet-registration/phone-number/{phone_number}", h.ServeHTTP)
-
- // test
- req, err := http.NewRequest("DELETE", "/wallet-registration/phone-number/foobar", nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- // assert response
- assert.Equal(t, http.StatusBadRequest, rr.Code)
- wantJson := `{
- "error": "The request was invalid in some way.",
- "extras": {
- "phone_number": "invalid phone number"
- }
- }`
- assert.JSONEq(t, wantJson, rr.Body.String())
- })
-
- t.Run("return 404 if network passphrase is testnet but phone number does not exist", func(t *testing.T) {
- h := DeletePhoneNumberHandler{NetworkPassphrase: network.TestNetworkPassphrase, Models: models}
- r := chi.NewRouter()
- r.Delete("/wallet-registration/phone-number/{phone_number}", h.ServeHTTP)
-
- // test
- req, err := http.NewRequest("DELETE", "/wallet-registration/phone-number/+14153333333", nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- // assert response
- assert.Equal(t, http.StatusNotFound, rr.Code)
- wantJson := `{"error":"Resource not found."}`
- assert.JSONEq(t, wantJson, rr.Body.String())
- })
-
- t.Run("return 204 if network passphrase is testnet and phone nymber exists", func(t *testing.T) {
- h := DeletePhoneNumberHandler{NetworkPassphrase: network.TestNetworkPassphrase, Models: models}
- r := chi.NewRouter()
- r.Delete("/wallet-registration/phone-number/{phone_number}", h.ServeHTTP)
-
- // test
- req, err := http.NewRequest("DELETE", "/wallet-registration/phone-number/"+receiver.PhoneNumber, nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- // assert response
- assert.Equal(t, http.StatusNoContent, rr.Code)
- wantJson := "null"
- assert.JSONEq(t, wantJson, rr.Body.String())
- })
-}
diff --git a/internal/serve/serve.go b/internal/serve/serve.go
index 972bbf7d2..a0a61fcf9 100644
--- a/internal/serve/serve.go
+++ b/internal/serve/serve.go
@@ -487,7 +487,7 @@ func handleHTTP(o ServeOptions) *chi.Mux {
})
// This will be used for test purposes and will only be available when IsPubnet is false:
- r.With(middleware.EnsureTenantMiddleware).Delete("/phone-number/{phone_number}", httphandler.DeletePhoneNumberHandler{
+ r.With(middleware.EnsureTenantMiddleware).Delete("/contact-info/{contact_info}", httphandler.DeleteContactInfoHandler{
Models: o.Models,
NetworkPassphrase: o.NetworkPassphrase,
}.ServeHTTP)
From 475fec8b53ae7bbdb5d0db036dbe7f9b54cfe33d Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 22 Oct 2024 10:47:42 -0700
Subject: [PATCH 42/75] [SDP-1339] Remove the word `phone` from the default
organization's otp_message_template (#439)
### What
Remove the word `phone` from the default organization's otp_message_template
### Why
Since we now allow email as a form of delivery of the OTP,
This PR addresses https://stellarorg.atlassian.net/browse/SDP-1339.
---
...te-default-value-of-otp-message-template.sql | 17 +++++++++++++++++
internal/data/organizations.go | 2 +-
internal/data/organizations_test.go | 2 +-
.../serve/httphandler/profile_handler_test.go | 2 +-
.../receiver_send_otp_handler_test.go | 8 ++++----
5 files changed, 24 insertions(+), 7 deletions(-)
create mode 100644 db/migrations/sdp-migrations/2024-10-18.0-update-default-value-of-otp-message-template.sql
diff --git a/db/migrations/sdp-migrations/2024-10-18.0-update-default-value-of-otp-message-template.sql b/db/migrations/sdp-migrations/2024-10-18.0-update-default-value-of-otp-message-template.sql
new file mode 100644
index 000000000..cce82edf3
--- /dev/null
+++ b/db/migrations/sdp-migrations/2024-10-18.0-update-default-value-of-otp-message-template.sql
@@ -0,0 +1,17 @@
+-- +migrate Up
+
+ALTER TABLE organizations
+ ALTER COLUMN otp_message_template SET DEFAULT '{{.OTP}} is your {{.OrganizationName}} verification code.';
+
+UPDATE organizations
+ SET otp_message_template = '{{.OTP}} is your {{.OrganizationName}} verification code.'
+ WHERE otp_message_template = '{{.OTP}} is your {{.OrganizationName}} phone verification code.';
+
+-- +migrate Down
+
+ALTER TABLE organizations
+ ALTER COLUMN otp_message_template SET DEFAULT '{{.OTP}} is your {{.OrganizationName}} phone verification code.';
+
+UPDATE organizations
+ SET otp_message_template = '{{.OTP}} is your {{.OrganizationName}} phone verification code.'
+ WHERE otp_message_template = '{{.OTP}} is your {{.OrganizationName}} verification code.';
diff --git a/internal/data/organizations.go b/internal/data/organizations.go
index ba56a9e7c..43b954284 100644
--- a/internal/data/organizations.go
+++ b/internal/data/organizations.go
@@ -26,7 +26,7 @@ import (
const (
DefaultReceiverRegistrationMessageTemplate = "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register."
- DefaultOTPMessageTemplate = "{{.OTP}} is your {{.OrganizationName}} phone verification code."
+ DefaultOTPMessageTemplate = "{{.OTP}} is your {{.OrganizationName}} verification code."
)
type Organization struct {
diff --git a/internal/data/organizations_test.go b/internal/data/organizations_test.go
index 636ad53f9..57529d94c 100644
--- a/internal/data/organizations_test.go
+++ b/internal/data/organizations_test.go
@@ -364,7 +364,7 @@ func Test_Organizations_Update(t *testing.T) {
t.Run("updates the organization's OTPMessageTemplate", func(t *testing.T) {
defer resetOrganizationInfo(t, ctx, dbConnectionPool)
- defaultMessage := "{{.OTP}} is your {{.OrganizationName}} phone verification code."
+ defaultMessage := "{{.OTP}} is your {{.OrganizationName}} verification code."
o, err := organizationModel.Get(ctx)
require.NoError(t, err)
assert.Equal(t, defaultMessage, o.OTPMessageTemplate)
diff --git a/internal/serve/httphandler/profile_handler_test.go b/internal/serve/httphandler/profile_handler_test.go
index 59df26a62..f965d2d7a 100644
--- a/internal/serve/httphandler/profile_handler_test.go
+++ b/internal/serve/httphandler/profile_handler_test.go
@@ -402,7 +402,7 @@ func Test_ProfileHandler_PatchOrganizationProfile_Successful(t *testing.T) {
},
resultingFieldsToCompare: map[string]interface{}{
"ReceiverRegistrationMessageTemplate": "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register.",
- "OTPMessageTemplate": "{{.OTP}} is your {{.OrganizationName}} phone verification code.",
+ "OTPMessageTemplate": "{{.OTP}} is your {{.OrganizationName}} verification code.",
"ReceiverInvitationResendIntervalDays": nilInt64,
"PaymentCancellationPeriodDays": nilInt64,
"PrivacyPolicyLink": nilString,
diff --git a/internal/serve/httphandler/receiver_send_otp_handler_test.go b/internal/serve/httphandler/receiver_send_otp_handler_test.go
index 1ddcb0571..27d28b8dc 100644
--- a/internal/serve/httphandler/receiver_send_otp_handler_test.go
+++ b/internal/serve/httphandler/receiver_send_otp_handler_test.go
@@ -321,7 +321,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) {
Once().
Run(func(args mock.Arguments) {
msg := args.Get(1).(message.Message)
- assert.Contains(t, msg.Message, "is your MyCustomAid phone verification code.")
+ assert.Contains(t, msg.Message, "is your MyCustomAid verification code.")
assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message)
})
},
@@ -373,7 +373,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) {
Once().
Run(func(args mock.Arguments) {
msg := args.Get(1).(message.Message)
- assert.Contains(t, msg.Message, "is your MyCustomAid phone verification code.")
+ assert.Contains(t, msg.Message, "is your MyCustomAid verification code.")
assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message)
})
},
@@ -480,12 +480,12 @@ func Test_ReceiverSendOTPHandler_sendOTP(t *testing.T) {
{
name: "dispacher fails",
overrideOrgOTPTemplate: defaultOTPMessageTemplate,
- wantMessage: fmt.Sprintf("246810 is your %s phone verification code. If you did not request this code, please ignore. Do not share your code with anyone.", organization.Name),
+ wantMessage: fmt.Sprintf("246810 is your %s verification code. If you did not request this code, please ignore. Do not share your code with anyone.", organization.Name),
},
{
name: "๐ successful with default message",
overrideOrgOTPTemplate: defaultOTPMessageTemplate,
- wantMessage: fmt.Sprintf("246810 is your %s phone verification code. If you did not request this code, please ignore. Do not share your code with anyone.", organization.Name),
+ wantMessage: fmt.Sprintf("246810 is your %s verification code. If you did not request this code, please ignore. Do not share your code with anyone.", organization.Name),
},
{
name: "๐ successful with custom message and pre-existing OTP tag",
From 28dcf411b7c7f9ef8002239a46764058e473cddc Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 22 Oct 2024 11:06:43 -0700
Subject: [PATCH 43/75] [SDP-1319] Update integration tests to test receivers
that were registered through email (#442)
### What
Update integration tests to test receivers that were registered through email
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1319.
---
.github/workflows/e2e_integration_test.yml | 15 +++++++++++----
.../docker/docker-compose-e2e-tests.yml | 2 +-
internal/integrationtests/integration_tests.go | 1 +
.../resources/disbursement_instructions_email.csv | 2 ++
...ts.csv => disbursement_instructions_phone.csv} | 0
internal/integrationtests/server_api_test.go | 2 +-
internal/integrationtests/utils_test.go | 2 +-
7 files changed, 17 insertions(+), 7 deletions(-)
create mode 100644 internal/integrationtests/resources/disbursement_instructions_email.csv
rename internal/integrationtests/resources/{disbursement_integration_tests.csv => disbursement_instructions_phone.csv} (100%)
diff --git a/.github/workflows/e2e_integration_test.yml b/.github/workflows/e2e_integration_test.yml
index 4799061b1..de3d443ca 100644
--- a/.github/workflows/e2e_integration_test.yml
+++ b/.github/workflows/e2e_integration_test.yml
@@ -25,15 +25,22 @@ jobs:
max-parallel: 1
matrix:
platform:
- - "Stellar"
- - "Circle"
+ - "Stellar-phone" # Stellar distribution account where receivers are registered with their phone number
+ - "Stellar-email" # Stellar distribution account where receivers are registered with their email
+ - "Circle-phone" # Circle distribution account where receivers are registered with their email
include:
- - platform: "Stellar"
+ - platform: "Stellar-phone"
environment: "Receiver Registration - E2E Integration Tests (Stellar)"
DISTRIBUTION_ACCOUNT_TYPE: "DISTRIBUTION_ACCOUNT.STELLAR.ENV"
- - platform: "Circle"
+ DISBURSEMENT_CSV_FILE_NAME: "disbursement_instructions_phone.csv"
+ - platform: "Stellar-email"
+ environment: "Receiver Registration - E2E Integration Tests (Stellar)"
+ DISTRIBUTION_ACCOUNT_TYPE: "DISTRIBUTION_ACCOUNT.STELLAR.ENV"
+ DISBURSEMENT_CSV_FILE_NAME: "disbursement_instructions_email.csv"
+ - platform: "Circle-phone"
environment: "Receiver Registration - E2E Integration Tests (Circle)"
DISTRIBUTION_ACCOUNT_TYPE: "DISTRIBUTION_ACCOUNT.CIRCLE.DB_VAULT"
+ DISBURSEMENT_CSV_FILE_NAME: "disbursement_instructions_phone.csv"
environment: ${{ matrix.environment }}
env:
DISTRIBUTION_ACCOUNT_TYPE: ${{ matrix.DISTRIBUTION_ACCOUNT_TYPE }}
diff --git a/internal/integrationtests/docker/docker-compose-e2e-tests.yml b/internal/integrationtests/docker/docker-compose-e2e-tests.yml
index 11f5e81c8..e2f8d35b3 100644
--- a/internal/integrationtests/docker/docker-compose-e2e-tests.yml
+++ b/internal/integrationtests/docker/docker-compose-e2e-tests.yml
@@ -66,7 +66,7 @@ services:
RECEIVER_ACCOUNT_PUBLIC_KEY: GCDYFAJSZPH3RCXL6NWMMOY54CXNUBYFTDCBW7GGG6VPBW3WSDKSB2NU
RECEIVER_ACCOUNT_PRIVATE_KEY: SDSAVUWVNOFG2JEHKIWEUHAYIA6PLGEHLMHX2TMVKEQGZKOFQ7XXKDFE
DISBURSEMENT_CSV_FILE_PATH: resources
- DISBURSEMENT_CSV_FILE_NAME: disbursement_integration_tests.csv
+ DISBURSEMENT_CSV_FILE_NAME: ${DISBURSEMENT_CSV_FILE_NAME:-disbursement_instructions_phone.csv}
SERVER_API_BASE_URL: http://localhost:8000
ADMIN_SERVER_BASE_URL: http://localhost:8003
ADMIN_SERVER_ACCOUNT_ID: SDP-admin
diff --git a/internal/integrationtests/integration_tests.go b/internal/integrationtests/integration_tests.go
index 6cbc60adb..de40bbd8e 100644
--- a/internal/integrationtests/integration_tests.go
+++ b/internal/integrationtests/integration_tests.go
@@ -253,6 +253,7 @@ func (it *IntegrationTestsService) StartIntegrationTests(ctx context.Context, op
err = it.serverAPI.ReceiverRegistration(ctx, authSEP24Token, &data.ReceiverRegistrationRequest{
OTP: data.TestnetAlwaysValidOTP,
PhoneNumber: disbursementData[0].Phone,
+ Email: disbursementData[0].Email,
VerificationValue: disbursementData[0].VerificationValue,
VerificationField: disbursement.VerificationField,
ReCAPTCHAToken: opts.RecaptchaSiteKey,
diff --git a/internal/integrationtests/resources/disbursement_instructions_email.csv b/internal/integrationtests/resources/disbursement_instructions_email.csv
new file mode 100644
index 000000000..e42b1f17c
--- /dev/null
+++ b/internal/integrationtests/resources/disbursement_instructions_email.csv
@@ -0,0 +1,2 @@
+email,id,amount,verification
+foobar@test.com,1,0.1,1999-03-30
\ No newline at end of file
diff --git a/internal/integrationtests/resources/disbursement_integration_tests.csv b/internal/integrationtests/resources/disbursement_instructions_phone.csv
similarity index 100%
rename from internal/integrationtests/resources/disbursement_integration_tests.csv
rename to internal/integrationtests/resources/disbursement_instructions_phone.csv
diff --git a/internal/integrationtests/server_api_test.go b/internal/integrationtests/server_api_test.go
index a3f84e5c4..e9a83c3f2 100644
--- a/internal/integrationtests/server_api_test.go
+++ b/internal/integrationtests/server_api_test.go
@@ -179,7 +179,7 @@ func Test_ProcessDisbursement(t *testing.T) {
HttpClient: &httpClientMock,
ServerApiBaseURL: "http://mock_server.com/",
DisbursementCSVFilePath: "resources",
- DisbursementCSVFileName: "disbursement_integration_tests.csv",
+ DisbursementCSVFileName: "disbursement_instructions_phone.csv",
}
ctx := context.Background()
diff --git a/internal/integrationtests/utils_test.go b/internal/integrationtests/utils_test.go
index d7bdc9afd..5d537ca39 100644
--- a/internal/integrationtests/utils_test.go
+++ b/internal/integrationtests/utils_test.go
@@ -52,7 +52,7 @@ func Test_readDisbursementCSV(t *testing.T) {
})
t.Run("reading csv file", func(t *testing.T) {
- data, err := readDisbursementCSV("resources", "disbursement_integration_tests.csv")
+ data, err := readDisbursementCSV("resources", "disbursement_instructions_phone.csv")
require.NoError(t, err)
assert.Equal(t, "0.1", data[0].Amount)
assert.NotNil(t, data[0].Phone)
From 42ae72aa6368176f25d75c79c5cf69f620d7e05c Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Wed, 23 Oct 2024 10:50:13 -0700
Subject: [PATCH 44/75] [SDP-1320] Refactor message.Message and its usage
(#440)
### What
Refactor message. The changes include:
- DRY: Share the email style throughout the email templates
- Rename template-related objects, methods, and files to make it explicit they refer to Staff and/or Email
- Rename `message.Message.Message` to `message.Message.Body` for improved clarity
- Update email templates by setting it's colors to black & white
### Why
Partially address https://stellarorg.atlassian.net/browse/SDP-1320.
---
cmd/auth.go | 6 +-
cmd/auth_test.go | 116 +++---------------
cmd/message.go | 2 +-
cmd/message_test.go | 2 +-
internal/htmltemplate/htmltemplate.go | 63 ++++++++--
internal/htmltemplate/htmltemplate_test.go | 18 +--
.../email/staff_forgot_password_message.tmpl | 16 +++
.../tmpl/email/staff_invitation_message.tmpl | 18 +++
.../staff_mfa_message.tmpl} | 16 +--
.../tmpl/forgot_password_message.tmpl | 27 ----
.../htmltemplate/tmpl/invitation_message.tmpl | 41 -------
.../tmpl/{ => pages}/receiver_register.tmpl | 0
.../receiver_registered_successfully.tmpl | 0
internal/message/aws_ses_client.go | 13 +-
internal/message/aws_ses_client_test.go | 10 +-
internal/message/aws_sns_client.go | 2 +-
internal/message/aws_sns_client_test.go | 4 +-
internal/message/dry_run_client.go | 2 +-
internal/message/dry_run_client_test.go | 4 +-
internal/message/message.go | 6 +-
internal/message/message_dispatcher.go | 2 +-
internal/message/message_dispatcher_test.go | 6 +-
internal/message/message_test.go | 28 ++---
internal/message/twilio_client.go | 2 +-
internal/message/twilio_client_test.go | 6 +-
...eceiver_wallets_sms_invitation_job_test.go | 4 +-
.../httphandler/forgot_password_handler.go | 6 +-
.../forgot_password_handler_test.go | 8 +-
internal/serve/httphandler/login_handler.go | 6 +-
.../serve/httphandler/login_handler_test.go | 4 +-
.../httphandler/receiver_send_otp_handler.go | 2 +-
.../receiver_send_otp_handler_test.go | 12 +-
.../serve/httphandler/user_handler_test.go | 8 +-
internal/services/send_invitation_message.go | 6 +-
.../services/send_invitation_message_test.go | 6 +-
.../send_receiver_wallets_invite_service.go | 4 +-
...nd_receiver_wallets_invite_service_test.go | 20 +--
37 files changed, 212 insertions(+), 284 deletions(-)
create mode 100644 internal/htmltemplate/tmpl/email/staff_forgot_password_message.tmpl
create mode 100644 internal/htmltemplate/tmpl/email/staff_invitation_message.tmpl
rename internal/htmltemplate/tmpl/{mfa_message.tmpl => email/staff_mfa_message.tmpl} (54%)
delete mode 100644 internal/htmltemplate/tmpl/forgot_password_message.tmpl
delete mode 100644 internal/htmltemplate/tmpl/invitation_message.tmpl
rename internal/htmltemplate/tmpl/{ => pages}/receiver_register.tmpl (100%)
rename internal/htmltemplate/tmpl/{ => pages}/receiver_registered_successfully.tmpl (100%)
diff --git a/cmd/auth.go b/cmd/auth.go
index 7088d662a..e446c9a12 100644
--- a/cmd/auth.go
+++ b/cmd/auth.go
@@ -130,14 +130,14 @@ func (a *AuthCommand) Command() *cobra.Command {
log.Ctx(ctx).Fatalf("error getting organization data: %s", err.Error())
}
- invitationData := htmltemplate.InvitationMessageTemplate{
+ invitationData := htmltemplate.StaffInvitationEmailMessageTemplate{
FirstName: firstName,
Role: role,
ForgotPasswordLink: forgotPasswordLink,
OrganizationName: organization.Name,
}
- msgBody, err := htmltemplate.ExecuteHTMLTemplateForInvitationMessage(invitationData)
+ msgBody, err := htmltemplate.ExecuteHTMLTemplateForStaffInvitationEmailMessage(invitationData)
if err != nil {
log.Ctx(ctx).Fatalf("error executing invitation message template: %s", err.Error())
}
@@ -145,7 +145,7 @@ func (a *AuthCommand) Command() *cobra.Command {
err = emailMessengerClient.SendMessage(message.Message{
ToEmail: email,
Title: "Welcome to Stellar Disbursement Platform",
- Message: msgBody,
+ Body: msgBody,
})
if err != nil {
log.Ctx(ctx).Fatalf("error sending invitation message: %s", err.Error())
diff --git a/cmd/auth_test.go b/cmd/auth_test.go
index 4f6d18ca0..ab0c3b89d 100644
--- a/cmd/auth_test.go
+++ b/cmd/auth_test.go
@@ -74,53 +74,13 @@ func Test_persistentPostRun(t *testing.T) {
err = rootCmd.Execute()
require.NoError(t, err)
- expectContains := `-------------------------------------------------------------------------------
-Recipient: email@email.com
-Subject: Welcome to Stellar Disbursement Platform
-Content:
-
-
-
- Welcome to Stellar Disbursement Platform
-
-
-
- Hello, First!
- You have been added to your organization's Stellar Disbursement Platform as a developer. Please click the link below to set up your password and let your organization administrator know if you have any questions.
-
- Set up my password
-
- Best regards,
- The MyCustomAid Team
-
-
-
--------------------------------------------------------------------------------
-`
+ expectContainsSlice := []string{
+ "Welcome to Stellar Disbursement Platform",
+ "You have been added to your organization's Stellar Disbursement Platform as a developer. Please click the button below to set up your password. If you have any questions, feel free to contact your organization administrator.
",
+ `Set up my password`,
+ "Best regards,
",
+ "The MyCustomAid Team
",
+ }
w.Close()
os.Stdout = stdOut
@@ -129,7 +89,9 @@ Content:
_, err = io.Copy(buf, r)
require.NoError(t, err)
- assert.Contains(t, buf.String(), expectContains)
+ for _, expectContains := range expectContainsSlice {
+ assert.Contains(t, buf.String(), expectContains)
+ }
// Set another SDP UI base URL
rootCmd.SetArgs([]string{"auth", "add-user", "email@email.com", "First", "Last", "--roles", "developer", "--sdp-ui-base-url", "https://sdp-ui.org", "--tenant-id", tnt.ID})
@@ -144,53 +106,13 @@ Content:
err = rootCmd.Execute()
require.NoError(t, err)
- expectContains = `-------------------------------------------------------------------------------
-Recipient: email@email.com
-Subject: Welcome to Stellar Disbursement Platform
-Content:
-
-
-
- Welcome to Stellar Disbursement Platform
-
-
-
- Hello, First!
- You have been added to your organization's Stellar Disbursement Platform as a developer. Please click the link below to set up your password and let your organization administrator know if you have any questions.
-
- Set up my password
-
- Best regards,
- The MyCustomAid Team
-
-
-
--------------------------------------------------------------------------------
-`
+ expectContainsSlice = []string{
+ "Welcome to Stellar Disbursement Platform",
+ "You have been added to your organization's Stellar Disbursement Platform as a developer. Please click the button below to set up your password. If you have any questions, feel free to contact your organization administrator.
",
+ `Set up my password`,
+ "Best regards,
",
+ "The MyCustomAid Team
",
+ }
w.Close()
os.Stdout = stdOut
@@ -199,5 +121,7 @@ Content:
_, err = io.Copy(buf, r)
require.NoError(t, err)
- assert.Contains(t, buf.String(), expectContains)
+ for _, expectContains := range expectContainsSlice {
+ assert.Contains(t, buf.String(), expectContains)
+ }
}
diff --git a/cmd/message.go b/cmd/message.go
index f68108f02..0eec600e1 100644
--- a/cmd/message.go
+++ b/cmd/message.go
@@ -115,7 +115,7 @@ func (s *MessageCommand) sendMessageCommand(messengerService MessengerServiceInt
Name: "message",
Usage: "The text of the message to be sent",
OptType: types.String,
- ConfigKey: &msg.Message,
+ ConfigKey: &msg.Body,
Required: true,
},
}
diff --git a/cmd/message_test.go b/cmd/message_test.go
index b5f827958..1b7067a12 100644
--- a/cmd/message_test.go
+++ b/cmd/message_test.go
@@ -93,7 +93,7 @@ func Test_message_send_SendMessage_wasCalled(t *testing.T) {
}
wantMessage := message.Message{
ToPhoneNumber: "+41555511111",
- Message: "hello world",
+ Body: "hello world",
}
mMessageService.On("SendMessage", wantMessageOptions, wantMessage).Return(nil).Once()
diff --git a/internal/htmltemplate/htmltemplate.go b/internal/htmltemplate/htmltemplate.go
index bf89d4755..baf555a1c 100644
--- a/internal/htmltemplate/htmltemplate.go
+++ b/internal/htmltemplate/htmltemplate.go
@@ -7,11 +7,19 @@ import (
"html/template"
)
-//go:embed tmpl/*.tmpl
+//go:embed tmpl/**/*.tmpl "tmpl/*.tmpl"
var Tmpl embed.FS
func ExecuteHTMLTemplate(templateName string, data interface{}) (string, error) {
- t, err := template.ParseFS(Tmpl, "tmpl/*.tmpl")
+ // Define the function map that will be available inside the templates
+ funcMap := template.FuncMap{
+ "EmailStyle": func() template.HTML {
+ return emailStyle
+ },
+ }
+
+ // Parse the templates with the function map
+ t, err := template.New("").Funcs(funcMap).ParseFS(Tmpl, "tmpl/*.tmpl", "tmpl/**/*.tmpl")
if err != nil {
return "", fmt.Errorf("error parsing embedded template files: %w", err)
}
@@ -33,32 +41,65 @@ func ExecuteHTMLTemplateForEmailEmptyBody(data EmptyBodyEmailTemplate) (string,
return ExecuteHTMLTemplate("empty_body.tmpl", data)
}
-type InvitationMessageTemplate struct {
+type StaffInvitationEmailMessageTemplate struct {
FirstName string
Role string
ForgotPasswordLink string
OrganizationName string
}
-func ExecuteHTMLTemplateForInvitationMessage(data InvitationMessageTemplate) (string, error) {
- return ExecuteHTMLTemplate("invitation_message.tmpl", data)
+func ExecuteHTMLTemplateForStaffInvitationEmailMessage(data StaffInvitationEmailMessageTemplate) (string, error) {
+ return ExecuteHTMLTemplate("staff_invitation_message.tmpl", data)
}
-type ForgotPasswordMessageTemplate struct {
+type StaffForgotPasswordEmailMessageTemplate struct {
ResetToken string
ResetPasswordLink string
OrganizationName string
}
-func ExecuteHTMLTemplateForForgotPasswordMessage(data ForgotPasswordMessageTemplate) (string, error) {
- return ExecuteHTMLTemplate("forgot_password_message.tmpl", data)
+func ExecuteHTMLTemplateForStaffForgotPasswordEmailMessage(data StaffForgotPasswordEmailMessageTemplate) (string, error) {
+ return ExecuteHTMLTemplate("staff_forgot_password_message.tmpl", data)
}
-type MFAMessageTemplate struct {
+type StaffMFAEmailMessageTemplate struct {
MFACode string
OrganizationName string
}
-func ExecuteHTMLTemplateForMFAMessage(data MFAMessageTemplate) (string, error) {
- return ExecuteHTMLTemplate("mfa_message.tmpl", data)
+func ExecuteHTMLTemplateForStaffMFAEmailMessage(data StaffMFAEmailMessageTemplate) (string, error) {
+ return ExecuteHTMLTemplate("staff_mfa_message.tmpl", data)
}
+
+// emailStyle is the CSS style that will be included in the email templates.
+const emailStyle = template.HTML(`
+
+`)
diff --git a/internal/htmltemplate/htmltemplate_test.go b/internal/htmltemplate/htmltemplate_test.go
index 7c92462eb..bc9e33657 100644
--- a/internal/htmltemplate/htmltemplate_test.go
+++ b/internal/htmltemplate/htmltemplate_test.go
@@ -50,16 +50,16 @@ func Test_ExecuteHTMLTemplateForEmailEmptyBody(t *testing.T) {
require.Contains(t, templateStr, randomStr)
}
-func Test_ExecuteHTMLTemplateForInvitationMessage(t *testing.T) {
+func Test_ExecuteHTMLTemplateForStaffInvitationEmailMessage(t *testing.T) {
forgotPasswordLink := "https://sdp.com/forgot-password"
- data := InvitationMessageTemplate{
+ data := StaffInvitationEmailMessageTemplate{
FirstName: "First",
Role: "developer",
ForgotPasswordLink: forgotPasswordLink,
OrganizationName: "Organization Name",
}
- content, err := ExecuteHTMLTemplateForInvitationMessage(data)
+ content, err := ExecuteHTMLTemplateForStaffInvitationEmailMessage(data)
require.NoError(t, err)
assert.Contains(t, content, "Hello, First!")
@@ -68,16 +68,16 @@ func Test_ExecuteHTMLTemplateForInvitationMessage(t *testing.T) {
assert.Contains(t, content, "Organization Name")
}
-func Test_ExecuteHTMLTemplateForInvitationMessage_HTMLInjectionAttack(t *testing.T) {
+func Test_ExecuteHTMLTemplateForStaffInvitationEmailMessage_HTMLInjectionAttack(t *testing.T) {
forgotPasswordLink := "https://sdp.com/forgot-password"
- data := InvitationMessageTemplate{
+ data := StaffInvitationEmailMessageTemplate{
FirstName: "First",
Role: "developer",
ForgotPasswordLink: forgotPasswordLink,
OrganizationName: "Redeem funds",
}
- content, err := ExecuteHTMLTemplateForInvitationMessage(data)
+ content, err := ExecuteHTMLTemplateForStaffInvitationEmailMessage(data)
require.NoError(t, err)
assert.Contains(t, content, "Hello, First!")
@@ -86,13 +86,13 @@ func Test_ExecuteHTMLTemplateForInvitationMessage_HTMLInjectionAttack(t *testing
assert.Contains(t, content, "<a href='evil.com'>Redeem funds</a>")
}
-func Test_ExecuteHTMLTemplateForForgotPasswordMessage(t *testing.T) {
- data := ForgotPasswordMessageTemplate{
+func Test_ExecuteHTMLTemplateForStaffForgotPasswordEmailMessage(t *testing.T) {
+ data := StaffForgotPasswordEmailMessageTemplate{
ResetToken: "resetToken",
ResetPasswordLink: "https://sdp.com/reset-password",
OrganizationName: "Organization Name",
}
- content, err := ExecuteHTMLTemplateForForgotPasswordMessage(data)
+ content, err := ExecuteHTMLTemplateForStaffForgotPasswordEmailMessage(data)
require.NoError(t, err)
assert.Contains(t, content, "resetToken")
diff --git a/internal/htmltemplate/tmpl/email/staff_forgot_password_message.tmpl b/internal/htmltemplate/tmpl/email/staff_forgot_password_message.tmpl
new file mode 100644
index 000000000..faf38e8fa
--- /dev/null
+++ b/internal/htmltemplate/tmpl/email/staff_forgot_password_message.tmpl
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Password Reset
+ {{EmailStyle}}
+
+
+ Hello,
+ We received a request to reset your {{.OrganizationName}} account password. Please use the confirmation token {{.ResetToken}} on the reset password page to create a new password.
+ If you did not request a password reset, please ignore this message or contact your organization administrator with any questions or concerns.
+ Best regards,
+ The {{.OrganizationName}} Team
+
+
diff --git a/internal/htmltemplate/tmpl/email/staff_invitation_message.tmpl b/internal/htmltemplate/tmpl/email/staff_invitation_message.tmpl
new file mode 100644
index 000000000..fd8e0f755
--- /dev/null
+++ b/internal/htmltemplate/tmpl/email/staff_invitation_message.tmpl
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Welcome to Stellar Disbursement Platform
+ {{EmailStyle}}
+
+
+ Hello, {{.FirstName}}!
+ You have been added to your organization's Stellar Disbursement Platform as a {{.Role}}. Please click the button below to set up your password. If you have any questions, feel free to contact your organization administrator.
+
+ Set up my password
+
+ Best regards,
+ The {{.OrganizationName}} Team
+
+
diff --git a/internal/htmltemplate/tmpl/mfa_message.tmpl b/internal/htmltemplate/tmpl/email/staff_mfa_message.tmpl
similarity index 54%
rename from internal/htmltemplate/tmpl/mfa_message.tmpl
rename to internal/htmltemplate/tmpl/email/staff_mfa_message.tmpl
index cc66a1855..98418a1b8 100644
--- a/internal/htmltemplate/tmpl/mfa_message.tmpl
+++ b/internal/htmltemplate/tmpl/email/staff_mfa_message.tmpl
@@ -2,22 +2,14 @@
+
Your verification code
-
+ {{EmailStyle}}
- Here is the 6-digit verification code you requested. Please enter it into the two-factor authentication prompt to sign-in.
+ Here is the 6-digit verification code you requested. Please enter it into the two-factor authentication prompt to sign-in:
Your verification code is: {{.MFACode}}.
- If you did not request this code, please ignore this message and consider changing your password to ensure the security of your account. If you have any questions or concerns, please reach out to your organization's administrator.
+ If you did not request this code, please ignore this message and consider updating your password to ensure account security. Contact your organization's administrator if you have any questions or concerns.
Best regards,
The {{.OrganizationName}} Team
diff --git a/internal/htmltemplate/tmpl/forgot_password_message.tmpl b/internal/htmltemplate/tmpl/forgot_password_message.tmpl
deleted file mode 100644
index e27d2a571..000000000
--- a/internal/htmltemplate/tmpl/forgot_password_message.tmpl
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- Password Reset
-
-
-
- Hello,
- We received a request to reset your Stellar Disbursement Platform account password. Please use the confirmation token {{.ResetToken}} on the reset password page to create a new password.
- If you did not request a password reset, please ignore this message or reach out to your organization's administrator with any questions or concerns.
- Best regards,
- The {{.OrganizationName}} Team
-
-
diff --git a/internal/htmltemplate/tmpl/invitation_message.tmpl b/internal/htmltemplate/tmpl/invitation_message.tmpl
deleted file mode 100644
index 3d106c831..000000000
--- a/internal/htmltemplate/tmpl/invitation_message.tmpl
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
- Welcome to Stellar Disbursement Platform
-
-
-
- Hello, {{.FirstName}}!
- You have been added to your organization's Stellar Disbursement Platform as a {{.Role}}. Please click the link below to set up your password and let your organization administrator know if you have any questions.
-
- Set up my password
-
- Best regards,
- The {{.OrganizationName}} Team
-
-
diff --git a/internal/htmltemplate/tmpl/receiver_register.tmpl b/internal/htmltemplate/tmpl/pages/receiver_register.tmpl
similarity index 100%
rename from internal/htmltemplate/tmpl/receiver_register.tmpl
rename to internal/htmltemplate/tmpl/pages/receiver_register.tmpl
diff --git a/internal/htmltemplate/tmpl/receiver_registered_successfully.tmpl b/internal/htmltemplate/tmpl/pages/receiver_registered_successfully.tmpl
similarity index 100%
rename from internal/htmltemplate/tmpl/receiver_registered_successfully.tmpl
rename to internal/htmltemplate/tmpl/pages/receiver_registered_successfully.tmpl
diff --git a/internal/message/aws_ses_client.go b/internal/message/aws_ses_client.go
index 9a3e798c7..3d127000f 100644
--- a/internal/message/aws_ses_client.go
+++ b/internal/message/aws_ses_client.go
@@ -52,9 +52,14 @@ func (a *awsSESClient) SendMessage(message Message) error {
// generateAWSEmail generates the email object to send an email through AWS SES.
func generateAWSEmail(message Message, sender string) (*ses.SendEmailInput, error) {
- html, err := htmltemplate.ExecuteHTMLTemplateForEmailEmptyBody(htmltemplate.EmptyBodyEmailTemplate{Body: template.HTML(message.Message)})
- if err != nil {
- return nil, fmt.Errorf("generating html template: %w", err)
+ emailBody := message.Body
+ var err error
+ // If the email body does not contain an HTML tag, then it is considered as a plain text email:
+ if !strings.Contains(emailBody, "
Date: Sat, 26 Oct 2024 11:42:33 +0100
Subject: [PATCH 45/75] [SDP-970][#102] Add Twilio SendGrid Email Client
(#444)
* SDP-970 add Twilio SendGrid Email Client
* SDP-970 PR feedback
---
README.md | 5 +-
cmd/message.go | 2 +-
cmd/serve.go | 1 +
cmd/utils/shared_config_options.go | 15 ++
.../2024-10-25.0-add-twilio-email.sql | 22 ++
go.list | 2 +
go.mod | 2 +
go.sum | 4 +
helmchart/sdp/README.md | 2 +-
helmchart/sdp/values.yaml | 2 +-
internal/message/main.go | 12 +-
internal/message/main_test.go | 1 +
internal/message/twilio_sendgrid_client.go | 84 ++++++++
.../message/twilio_sendgrid_client_test.go | 203 ++++++++++++++++++
14 files changed, 349 insertions(+), 8 deletions(-)
create mode 100644 db/migrations/sdp-migrations/2024-10-25.0-add-twilio-email.sql
create mode 100644 internal/message/twilio_sendgrid_client.go
create mode 100644 internal/message/twilio_sendgrid_client_test.go
diff --git a/README.md b/README.md
index 85d248359..3c9a5051b 100644
--- a/README.md
+++ b/README.md
@@ -118,9 +118,10 @@ The Message Service sends messages to users and recipients for the following rea
- Providing one-time passcodes (OTPs) to recipients
- Sending emails to users during account creation and account recovery flows
-Note that the Message Service requires that both SMS and email services are configured. For emails, AWS SES is supported. For SMS messages to recipients, Twilio is supported. AWS SNS support is not integrated yet.
+Note that the Message Service requires that both SMS and email services are configured. For emails, AWS SES and Twilio Sendgrid are supported. For SMS messages to recipients, Twilio and AWS SNS are supported.
+
+If you're using the `AWS_EMAIL` or `TWILIO_EMAIL` sender types, you'll need to verify the email address you're using to send emails in order to prevent it from being flagged by email firewalls. You can do that by following the instructions in [this link for AWS SES](https://docs.aws.amazon.com/ses/latest/dg/email-authentication-methods.html) or [this link for Twilio Sendgrid](https://www.twilio.com/docs/sendgrid/glossary/sender-authentication).
-If you're using the `AWS_EMAIL` sender type, you'll need to verify the email address you're using to send emails in order to prevent it from being flagged by email firewalls. You can do that by following the instructions in [this link](https://docs.aws.amazon.com/ses/latest/dg/email-authentication-methods.html).
#### Wallet Registration UI
diff --git a/cmd/message.go b/cmd/message.go
index 0eec600e1..ad782f5e4 100644
--- a/cmd/message.go
+++ b/cmd/message.go
@@ -40,7 +40,7 @@ func (s *MessageCommand) Command(messengerService MessengerServiceInterface) *co
// message sender type
{
Name: "message-sender-type",
- Usage: `Message Sender Type. Options: "TWILIO_SMS", "AWS_SMS", "AWS_EMAIL", "DRY_RUN"`,
+ Usage: `Message Sender Type. Options: "TWILIO_SMS", "TWILIO_EMAIL", AWS_SMS", "AWS_EMAIL", "DRY_RUN"`,
OptType: types.String,
CustomSetValue: cmdUtils.SetConfigOptionMessengerType,
ConfigKey: &opts.MessengerType,
diff --git a/cmd/serve.go b/cmd/serve.go
index ee96e1b68..355cdf7fc 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -150,6 +150,7 @@ func (s *ServerService) SetupConsumers(ctx context.Context, o SetupConsumersOpti
MessageDispatcher: o.ServeOpts.MessageDispatcher,
MaxInvitationResendAttempts: int64(o.ServeOpts.MaxInvitationResendAttempts),
Sep10SigningPrivateKey: o.ServeOpts.Sep10SigningPrivateKey,
+ CrashTrackerClient: o.ServeOpts.CrashTrackerClient.Clone(),
}),
)
if err != nil {
diff --git a/cmd/utils/shared_config_options.go b/cmd/utils/shared_config_options.go
index c4bddebb6..6b06b8849 100644
--- a/cmd/utils/shared_config_options.go
+++ b/cmd/utils/shared_config_options.go
@@ -43,6 +43,21 @@ func TwilioConfigOptions(opts *message.MessengerOptions) []*config.ConfigOption
ConfigKey: &opts.TwilioServiceSID,
Required: false,
},
+ // Twilio Email (SendGrid)
+ {
+ Name: "twilio-sendgrid-api-key",
+ Usage: "The API key of the Twilio SendGrid account",
+ OptType: types.String,
+ ConfigKey: &opts.TwilioSendGridAPIKey,
+ Required: false,
+ },
+ {
+ Name: "twilio-sendgrid-sender-address",
+ Usage: "The email address that Twilio SendGrid will use to send emails",
+ OptType: types.String,
+ ConfigKey: &opts.TwilioSendGridSenderAddress,
+ Required: false,
+ },
}
}
diff --git a/db/migrations/sdp-migrations/2024-10-25.0-add-twilio-email.sql b/db/migrations/sdp-migrations/2024-10-25.0-add-twilio-email.sql
new file mode 100644
index 000000000..8900e0481
--- /dev/null
+++ b/db/migrations/sdp-migrations/2024-10-25.0-add-twilio-email.sql
@@ -0,0 +1,22 @@
+-- This is to add `TWILIO_EMAIL` to the `message_type` enum.
+
+-- +migrate Up
+ALTER TYPE message_type ADD VALUE 'TWILIO_EMAIL';
+
+
+-- +migrate Down
+CREATE TYPE temp_message_type AS ENUM (
+ 'TWILIO_SMS',
+ 'AWS_SMS',
+ 'AWS_EMAIL',
+ 'DRY_RUN'
+ );
+
+DELETE FROM messages WHERE type = 'TWILIO_EMAIL';
+
+ALTER TABLE messages
+ ALTER COLUMN type TYPE temp_message_type USING type::text::temp_message_type;
+
+DROP TYPE message_type;
+
+ALTER TYPE temp_message_type RENAME TO message_type;
\ No newline at end of file
diff --git a/go.list b/go.list
index 979f929dd..69cc2ab6e 100644
--- a/go.list
+++ b/go.list
@@ -215,6 +215,8 @@ github.com/sanity-io/litter v1.5.5
github.com/schollz/closestmatch v2.1.0+incompatible
github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2
github.com/segmentio/kafka-go v0.4.47
+github.com/sendgrid/rest v2.6.9+incompatible
+github.com/sendgrid/sendgrid-go v3.16.0+incompatible
github.com/sergi/go-diff v1.2.0
github.com/shopspring/decimal v1.3.1
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
diff --git a/go.mod b/go.mod
index 49df7aa76..c0a66ef0f 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,8 @@ require (
github.com/rs/cors v1.11.1
github.com/rubenv/sql-migrate v1.7.0
github.com/segmentio/kafka-go v0.4.47
+ github.com/sendgrid/rest v2.6.9+incompatible
+ github.com/sendgrid/sendgrid-go v3.16.0+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
diff --git a/go.sum b/go.sum
index 8709e494a..09b09a352 100644
--- a/go.sum
+++ b/go.sum
@@ -161,6 +161,10 @@ github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBK
github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y=
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
+github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
+github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
+github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
+github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
diff --git a/helmchart/sdp/README.md b/helmchart/sdp/README.md
index a1965a58f..c89bdadaa 100644
--- a/helmchart/sdp/README.md
+++ b/helmchart/sdp/README.md
@@ -126,7 +126,7 @@ Configuration parameters for the SDP Core Service which is the core backend serv
| `sdp.configMap.data.SEP10_SIGNING_PUBLIC_KEY` | Anchor platform SEP10 signing public key. | `nil` |
| `sdp.configMap.data.DISTRIBUTION_PUBLIC_KEY` | The public key of the HOST's Stellar distribution account, used to create channel accounts. | `nil` |
| `sdp.configMap.data.METRICS_TYPE` | Defines the type of metrics system in use. Options: "PROMETHEUS". | `PROMETHEUS` |
-| `sdp.configMap.data.EMAIL_SENDER_TYPE` | The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL". | `DRY_RUN` |
+| `sdp.configMap.data.EMAIL_SENDER_TYPE` | The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL", "TWILIO_EMAIL". | `DRY_RUN` |
| `sdp.configMap.data.SMS_SENDER_TYPE` | The messenger type used to send text messages to recipients. Options: "DRY_RUN", "TWILIO_SMS". | `DRY_RUN` |
| `sdp.configMap.data.RECAPTCHA_SITE_KEY` | Site key for ReCaptcha. Required if using ReCaptcha. | `nil` |
| `sdp.configMap.data.CORS_ALLOWED_ORIGINS` | Specifies the domains allowed to make cross-origin requests. "*" means all domains are allowed. | `*` |
diff --git a/helmchart/sdp/values.yaml b/helmchart/sdp/values.yaml
index 2fae64a69..0318ee14a 100644
--- a/helmchart/sdp/values.yaml
+++ b/helmchart/sdp/values.yaml
@@ -142,7 +142,7 @@ sdp:
## @param sdp.configMap.data.SEP10_SIGNING_PUBLIC_KEY Anchor platform SEP10 signing public key.
## @param sdp.configMap.data.DISTRIBUTION_PUBLIC_KEY The public key of the HOST's Stellar distribution account, used to create channel accounts.
## @param sdp.configMap.data.METRICS_TYPE Defines the type of metrics system in use. Options: "PROMETHEUS".
- ## @param sdp.configMap.data.EMAIL_SENDER_TYPE The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL".
+ ## @param sdp.configMap.data.EMAIL_SENDER_TYPE The messenger type used to send invitations to new dashboard users. Options: "DRY_RUN", "AWS_EMAIL", "TWILIO_EMAIL".
## @param sdp.configMap.data.SMS_SENDER_TYPE The messenger type used to send text messages to recipients. Options: "DRY_RUN", "TWILIO_SMS".
## @param sdp.configMap.data.RECAPTCHA_SITE_KEY Site key for ReCaptcha. Required if using ReCaptcha.
## @param sdp.configMap.data.CORS_ALLOWED_ORIGINS Specifies the domains allowed to make cross-origin requests. "*" means all domains are allowed.
diff --git a/internal/message/main.go b/internal/message/main.go
index 9ed073d5e..c7b067ab7 100644
--- a/internal/message/main.go
+++ b/internal/message/main.go
@@ -12,6 +12,8 @@ type MessengerType string
const (
// MessengerTypeTwilioSMS is used to send SMS messages using Twilio.
MessengerTypeTwilioSMS MessengerType = "TWILIO_SMS"
+ // MessengerTypeTwilioEmail is used to send emails using Twilio SendGrid.
+ MessengerTypeTwilioEmail MessengerType = "TWILIO_EMAIL"
// MessengerTypeAWSSMS is used to send SMS messages using AWS SNS.
MessengerTypeAWSSMS MessengerType = "AWS_SMS"
// MessengerTypeAWSEmail is used to send emails using AWS SES.
@@ -21,7 +23,7 @@ const (
)
func (mt MessengerType) All() []MessengerType {
- return []MessengerType{MessengerTypeTwilioSMS, MessengerTypeAWSSMS, MessengerTypeAWSEmail, MessengerTypeDryRun}
+ return []MessengerType{MessengerTypeTwilioSMS, MessengerTypeTwilioEmail, MessengerTypeAWSSMS, MessengerTypeAWSEmail, MessengerTypeDryRun}
}
func ParseMessengerType(messengerTypeStr string) (MessengerType, error) {
@@ -40,7 +42,7 @@ func (mt MessengerType) ValidSMSTypes() []MessengerType {
}
func (mt MessengerType) ValidEmailTypes() []MessengerType {
- return []MessengerType{MessengerTypeDryRun, MessengerTypeAWSEmail}
+ return []MessengerType{MessengerTypeDryRun, MessengerTypeTwilioEmail, MessengerTypeAWSEmail}
}
func (mt MessengerType) IsSMS() bool {
@@ -59,6 +61,9 @@ type MessengerOptions struct {
TwilioAccountSID string
TwilioAuthToken string
TwilioServiceSID string
+ // Twilio Email (SendGrid)
+ TwilioSendGridAPIKey string
+ TwilioSendGridSenderAddress string
// AWS
AWSAccessKeyID string
@@ -74,10 +79,11 @@ func GetClient(opts MessengerOptions) (MessengerClient, error) {
switch opts.MessengerType {
case MessengerTypeTwilioSMS:
return NewTwilioClient(opts.TwilioAccountSID, opts.TwilioAuthToken, opts.TwilioServiceSID)
+ case MessengerTypeTwilioEmail:
+ return NewTwilioSendGridClient(opts.TwilioSendGridAPIKey, opts.TwilioSendGridSenderAddress)
case MessengerTypeAWSSMS:
return NewAWSSNSClient(opts.AWSAccessKeyID, opts.AWSSecretAccessKey, opts.AWSRegion, opts.AWSSNSSenderID)
-
case MessengerTypeAWSEmail:
return NewAWSSESClient(opts.AWSAccessKeyID, opts.AWSSecretAccessKey, opts.AWSRegion, opts.AWSSESSenderID)
diff --git a/internal/message/main_test.go b/internal/message/main_test.go
index 48a68fd81..99dd17077 100644
--- a/internal/message/main_test.go
+++ b/internal/message/main_test.go
@@ -16,6 +16,7 @@ func Test_ParseMessengerType(t *testing.T) {
{wantErr: fmt.Errorf("invalid message sender type \"\"")},
{messengerType: "foo_BAR", wantErr: fmt.Errorf("invalid message sender type \"FOO_BAR\"")},
{messengerType: "TWILIO_SMS"},
+ {messengerType: "TWILIO_EMAIL"},
{messengerType: "tWiLiO_SMS"},
{messengerType: "AWS_SMS"},
{messengerType: "AWS_EMAIL"},
diff --git a/internal/message/twilio_sendgrid_client.go b/internal/message/twilio_sendgrid_client.go
new file mode 100644
index 000000000..c1a3eefb1
--- /dev/null
+++ b/internal/message/twilio_sendgrid_client.go
@@ -0,0 +1,84 @@
+package message
+
+import (
+ "fmt"
+ "html/template"
+ "strings"
+
+ "github.com/sendgrid/rest"
+ "github.com/sendgrid/sendgrid-go"
+ "github.com/sendgrid/sendgrid-go/helpers/mail"
+ "github.com/stellar/go/support/log"
+
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/htmltemplate"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
+)
+
+type twilioSendGridInterface interface {
+ Send(email *mail.SGMailV3) (*rest.Response, error)
+}
+
+var _ twilioSendGridInterface = (*sendgrid.Client)(nil)
+
+type twilioSendGridClient struct {
+ client twilioSendGridInterface
+ senderAddress string
+}
+
+func (t *twilioSendGridClient) MessengerType() MessengerType {
+ return MessengerTypeTwilioEmail
+}
+
+func (t *twilioSendGridClient) SendMessage(message Message) error {
+ err := message.ValidateFor(t.MessengerType())
+ if err != nil {
+ return fmt.Errorf("validating message to send an email through SendGrid: %w", err)
+ }
+
+ from := mail.NewEmail("", t.senderAddress)
+ to := mail.NewEmail("", message.ToEmail)
+
+ emailBody := message.Body
+ if !strings.Contains(emailBody, "= 400 {
+ return fmt.Errorf("sendGrid API returned error status code= %d, body= %s",
+ response.StatusCode, response.Body)
+ }
+
+ log.Debugf("๐ SendGrid sent an email to the receiver %q", utils.TruncateString(message.ToEmail, 3))
+ return nil
+}
+
+// NewTwilioSendGridClient creates a new SendGrid client that is used to send emails
+func NewTwilioSendGridClient(apiKey string, senderAddress string) (MessengerClient, error) {
+ apiKey = strings.TrimSpace(apiKey)
+ if apiKey == "" {
+ return nil, fmt.Errorf("sendGrid API key is empty")
+ }
+
+ senderAddress = strings.TrimSpace(senderAddress)
+ if err := utils.ValidateEmail(senderAddress); err != nil {
+ return nil, fmt.Errorf("sendGrid senderAddress is invalid: %w", err)
+ }
+
+ return &twilioSendGridClient{
+ client: sendgrid.NewSendClient(apiKey),
+ senderAddress: senderAddress,
+ }, nil
+}
+
+var _ MessengerClient = (*twilioSendGridClient)(nil)
diff --git a/internal/message/twilio_sendgrid_client_test.go b/internal/message/twilio_sendgrid_client_test.go
new file mode 100644
index 000000000..507d0831c
--- /dev/null
+++ b/internal/message/twilio_sendgrid_client_test.go
@@ -0,0 +1,203 @@
+package message
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/sendgrid/rest"
+ "github.com/sendgrid/sendgrid-go/helpers/mail"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func (m *mockTwilioSendGridClient) Send(email *mail.SGMailV3) (*rest.Response, error) {
+ args := m.Called(email)
+ if args.Get(0) == nil {
+ return nil, args.Error(1)
+ }
+ return args.Get(0).(*rest.Response), args.Error(1)
+}
+
+func Test_NewTwilioSendGridClient(t *testing.T) {
+ testCases := []struct {
+ name string
+ apiKey string
+ senderAddress string
+ wantErr error
+ }{
+ {
+ name: "apiKey cannot be empty",
+ wantErr: fmt.Errorf("sendGrid API key is empty"),
+ },
+ {
+ name: "senderAddress needs to be a valid email",
+ apiKey: "api-key",
+ senderAddress: "invalid-email",
+ wantErr: fmt.Errorf("sendGrid senderAddress is invalid: the provided email is not valid"),
+ },
+ {
+ name: "all fields are present",
+ apiKey: "api-key",
+ senderAddress: "foo@stellar.org",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ _, err := NewTwilioSendGridClient(tc.apiKey, tc.senderAddress)
+ if tc.wantErr != nil {
+ assert.EqualError(t, err, tc.wantErr.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func Test_TwilioSendGridClient_SendMessage_messageIsInvalid(t *testing.T) {
+ var mSendGrid MessengerClient = &twilioSendGridClient{}
+ err := mSendGrid.SendMessage(Message{})
+ assert.EqualError(t, err, "validating message to send an email through SendGrid: invalid e-mail: invalid email format: email cannot be empty")
+}
+
+func Test_TwilioSendGridClient_SendMessage_errorIsHandledCorrectly(t *testing.T) {
+ message := Message{ToEmail: "foo@stellar.org", Title: "test title", Body: "foo bar"}
+
+ mSendGrid := newMockTwilioSendGridClient(t)
+
+ // MatchBy is used to match the email that is being sent
+ mSendGrid.On("Send", mock.MatchedBy(func(email *mail.SGMailV3) bool {
+ // Verify the email content matches what we expect
+ return email.From.Address == "sender@stellar.org" &&
+ email.Subject == message.Title &&
+ len(email.Personalizations) == 1 &&
+ len(email.Personalizations[0].To) == 1 &&
+ email.Personalizations[0].To[0].Address == message.ToEmail
+ })).Return(nil, fmt.Errorf("test SendGrid error")).Once()
+
+ client := &twilioSendGridClient{
+ client: mSendGrid,
+ senderAddress: "sender@stellar.org",
+ }
+
+ err := client.SendMessage(message)
+ assert.EqualError(t, err, "sending SendGrid email: test SendGrid error")
+}
+
+func Test_TwilioSendGridClient_SendMessage_handlesAPIError(t *testing.T) {
+ message := Message{ToEmail: "foo@stellar.org", Title: "test title", Body: "foo bar"}
+
+ mSendGrid := newMockTwilioSendGridClient(t)
+
+ mSendGrid.On("Send", mock.MatchedBy(func(email *mail.SGMailV3) bool {
+ return email.From.Address == "sender@stellar.org" &&
+ email.Subject == message.Title
+ })).Return(&rest.Response{
+ StatusCode: 400,
+ Body: "Bad Request",
+ }, nil).Once()
+
+ client := &twilioSendGridClient{
+ client: mSendGrid,
+ senderAddress: "sender@stellar.org",
+ }
+
+ err := client.SendMessage(message)
+ assert.EqualError(t, err, "sendGrid API returned error status code= 400, body= Bad Request")
+}
+
+func Test_TwilioSendGrid_SendMessage_success(t *testing.T) {
+ message := Message{ToEmail: "foo@stellar.org", Title: "test title", Body: "foo bar"}
+
+ mSendGrid := newMockTwilioSendGridClient(t)
+
+ successResponse := &rest.Response{
+ StatusCode: 202,
+ Body: "Accepted",
+ }
+
+ mSendGrid.On("Send", mock.MatchedBy(func(email *mail.SGMailV3) bool {
+ // Verify plain text was converted to HTML
+ expectedHTML := `
+
+
+
+
+
+
+
+
+foo bar
+
+
+`
+ expectedHTML = strings.TrimSpace(expectedHTML)
+
+ gotContent := email.Content[0].Value
+ gotContent = strings.TrimSpace(gotContent)
+
+ return email.From.Address == "sender@stellar.org" &&
+ email.Subject == message.Title &&
+ len(email.Personalizations) == 1 &&
+ len(email.Personalizations[0].To) == 1 &&
+ email.Personalizations[0].To[0].Address == message.ToEmail &&
+ gotContent == expectedHTML
+ })).Return(successResponse, nil).Once()
+
+ client := &twilioSendGridClient{
+ client: mSendGrid,
+ senderAddress: "sender@stellar.org",
+ }
+
+ err := client.SendMessage(message)
+ assert.NoError(t, err)
+}
+
+func Test_TwilioSendGrid_SendMessage_withHTMLContent(t *testing.T) {
+ htmlContent := "Hello
"
+ message := Message{ToEmail: "foo@stellar.org", Title: "test title", Body: htmlContent}
+
+ mSendGrid := newMockTwilioSendGridClient(t)
+
+ successResponse := &rest.Response{
+ StatusCode: 202,
+ Body: "Accepted",
+ }
+
+ mSendGrid.On("Send", mock.MatchedBy(func(email *mail.SGMailV3) bool {
+ gotContent := email.Content[0].Value
+ gotContent = strings.TrimSpace(gotContent)
+
+ return email.From.Address == "sender@stellar.org" &&
+ email.Subject == message.Title &&
+ gotContent == htmlContent // Should use original HTML content
+ })).Return(successResponse, nil).Once()
+
+ client := &twilioSendGridClient{
+ client: mSendGrid,
+ senderAddress: "sender@stellar.org",
+ }
+
+ err := client.SendMessage(message)
+ assert.NoError(t, err)
+}
+
+// mockTwilioSendGridClient implements twilioSendGridInterface for testing
+type mockTwilioSendGridClient struct {
+ mock.Mock
+}
+
+type testInterface interface {
+ mock.TestingT
+ Cleanup(func())
+}
+
+func newMockTwilioSendGridClient(t testInterface) *mockTwilioSendGridClient {
+ mock := &mockTwilioSendGridClient{}
+ mock.Mock.Test(t)
+
+ t.Cleanup(func() { mock.AssertExpectations(t) })
+
+ return mock
+}
From e93f80a28bf86e8dca3c1f3ee4086e5c1edd9425 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 28 Oct 2024 21:04:53 +0000
Subject: [PATCH 46/75] Bump github.com/twilio/twilio-go from 1.23.4 to 1.23.5
in the minor-and-patch group (#446)
---
go.list | 2 +-
go.mod | 2 +-
go.sum | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/go.list b/go.list
index 69cc2ab6e..0043fd36a 100644
--- a/go.list
+++ b/go.list
@@ -235,7 +235,7 @@ github.com/stretchr/testify v1.9.0
github.com/subosito/gotenv v1.6.0
github.com/tdewolff/minify/v2 v2.12.4
github.com/tdewolff/parse/v2 v2.6.4
-github.com/twilio/twilio-go v1.23.4
+github.com/twilio/twilio-go v1.23.5
github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8
github.com/ugorji/go/codec v1.2.7
github.com/urfave/negroni/v3 v3.1.1
diff --git a/go.mod b/go.mod
index c0a66ef0f..2043ff739 100644
--- a/go.mod
+++ b/go.mod
@@ -30,7 +30,7 @@ require (
github.com/spf13/viper v1.19.0
github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043
github.com/stretchr/testify v1.9.0
- github.com/twilio/twilio-go v1.23.4
+ github.com/twilio/twilio-go v1.23.5
golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
)
diff --git a/go.sum b/go.sum
index 09b09a352..db72d2afb 100644
--- a/go.sum
+++ b/go.sum
@@ -199,8 +199,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/twilio/twilio-go v1.23.4 h1:1JePQ9xWVaW7iZieEIcfm1E89nOPcgZ+I5ZRcukBkRY=
-github.com/twilio/twilio-go v1.23.4/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko=
+github.com/twilio/twilio-go v1.23.5 h1:5ksHynnYhjKf1vG7KK7+jujEj/DhQ1knwQAhNuDExW4=
+github.com/twilio/twilio-go v1.23.5/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
From 0fe26ecb99c314872ba01d5ecf6619d083fddccc Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 29 Oct 2024 12:24:04 -0700
Subject: [PATCH 47/75] [SDP-1365] Disable some headers (#448)
### What
Disable the following headers:
- X-XSS-Protection
- X-Forwarded-Host
- X-Real-IP
- True-Client-IP
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1365.
---
helmchart/sdp/values.yaml | 4 ++--
internal/serve/middleware/middleware_test.go | 1 -
internal/serve/serve.go | 1 -
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/helmchart/sdp/values.yaml b/helmchart/sdp/values.yaml
index 0318ee14a..0c875e64f 100644
--- a/helmchart/sdp/values.yaml
+++ b/helmchart/sdp/values.yaml
@@ -248,7 +248,7 @@ sdp:
enabled: true
className: "nginx"
annotations:
- nginx.ingress.kubernetes.io/custom-response-headers: "X-XSS-Protection: 1; mode=block || X-Frame-Options: DENY || X-Content-Type-Options: nosniff || Strict-Transport-Security: max-age=31536000; includeSubDomains"
+ nginx.ingress.kubernetes.io/custom-response-headers: "X-Frame-Options: DENY || X-Content-Type-Options: nosniff || Strict-Transport-Security: max-age=31536000; includeSubDomains"
nginx.ingress.kubernetes.io/limit-rpm: "120"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
tls:
@@ -401,7 +401,7 @@ anchorPlatform:
enabled: true
className: "nginx"
annotations:
- nginx.ingress.kubernetes.io/custom-response-headers: "X-XSS-Protection: 1; mode=block || X-Frame-Options: DENY || X-Content-Type-Options: nosniff || Strict-Transport-Security: max-age=31536000; includeSubDomains"
+ nginx.ingress.kubernetes.io/custom-response-headers: "X-Frame-Options: DENY || X-Content-Type-Options: nosniff || Strict-Transport-Security: max-age=31536000; includeSubDomains"
nginx.ingress.kubernetes.io/limit-rpm: "120"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
tls:
diff --git a/internal/serve/middleware/middleware_test.go b/internal/serve/middleware/middleware_test.go
index 09856d6c9..2e593c64f 100644
--- a/internal/serve/middleware/middleware_test.go
+++ b/internal/serve/middleware/middleware_test.go
@@ -13,7 +13,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/sirupsen/logrus"
-
"github.com/stellar/go/support/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
diff --git a/internal/serve/serve.go b/internal/serve/serve.go
index a0a61fcf9..48296a1a1 100644
--- a/internal/serve/serve.go
+++ b/internal/serve/serve.go
@@ -210,7 +210,6 @@ func handleHTTP(o ServeOptions) *chi.Mux {
httprate.WithKeyFuncs(httprate.KeyByIP, httprate.KeyByEndpoint),
))
mux.Use(chimiddleware.RequestID)
- mux.Use(chimiddleware.RealIP)
mux.Use(middleware.ResolveTenantFromRequestMiddleware(o.tenantManager, o.SingleTenantMode))
mux.Use(middleware.LoggingMiddleware)
mux.Use(middleware.RecoverHandler)
From 568f0ea9d8cd152ab08649e66b6665cd869d5458 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 29 Oct 2024 12:54:03 -0700
Subject: [PATCH 48/75] [SDP-1345] ensure instruction file has the .csv
extension (#443)
### What
Ensure the instruction file has the `.csv` extension.
Also, refactored some unit tests to make them less repetitive.
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1245.
---
go.list | 2 +-
go.mod | 1 +
go.sum | 4 +-
.../serve/httphandler/disbursement_handler.go | 54 +++--
.../httphandler/disbursement_handler_test.go | 226 ++++++++++++------
internal/utils/validation.go | 12 +
internal/utils/validation_test.go | 31 +++
7 files changed, 234 insertions(+), 96 deletions(-)
diff --git a/go.list b/go.list
index 0043fd36a..de5c081b0 100644
--- a/go.list
+++ b/go.list
@@ -275,7 +275,7 @@ go.uber.org/zap v1.21.0
golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d
golang.org/x/mod v0.17.0
-golang.org/x/net v0.26.0
+golang.org/x/net v0.30.0
golang.org/x/oauth2 v0.21.0
golang.org/x/sync v0.8.0
golang.org/x/sys v0.26.0
diff --git a/go.mod b/go.mod
index 2043ff739..e85f18791 100644
--- a/go.mod
+++ b/go.mod
@@ -73,6 +73,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
diff --git a/go.sum b/go.sum
index db72d2afb..6cc0fa705 100644
--- a/go.sum
+++ b/go.sum
@@ -248,8 +248,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
-golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go
index 1e224987b..4a31ce1eb 100644
--- a/internal/serve/httphandler/disbursement_handler.go
+++ b/internal/serve/httphandler/disbursement_handler.go
@@ -8,7 +8,9 @@ import (
"errors"
"fmt"
"io"
+ "mime/multipart"
"net/http"
+ "path/filepath"
"slices"
"strings"
"time"
@@ -26,6 +28,7 @@ import (
"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators"
"github.com/stellar/stellar-disbursement-platform-backend/internal/services"
"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
"github.com/stellar/stellar-disbursement-platform-backend/pkg/schema"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/auth"
)
@@ -211,17 +214,9 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter,
return
}
- // Parse uploaded CSV file
- file, header, err := r.FormFile("file")
- if err != nil {
- httperror.BadRequest("could not parse file", err, nil).Render(w)
- return
- }
- defer file.Close()
-
- var buf bytes.Buffer
- if _, err = io.Copy(&buf, file); err != nil {
- httperror.BadRequest("could not read file", err, nil).Render(w)
+ buf, header, httpErr := parseCsvFromMultipartRequest(r)
+ if httpErr != nil {
+ httpErr.Render(w)
return
}
@@ -267,11 +262,11 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter,
}); err != nil {
switch {
case errors.Is(err, data.ErrMaxInstructionsExceeded):
- httperror.BadRequest(fmt.Sprintf("number of instructions exceeds maximum of : %d", data.MaxInstructionsPerDisbursement), err, nil).Render(w)
+ httperror.BadRequest(fmt.Sprintf("number of instructions exceeds maximum of %d", data.MaxInstructionsPerDisbursement), err, nil).Render(w)
case errors.Is(err, data.ErrReceiverVerificationMismatch):
httperror.BadRequest(errors.Unwrap(err).Error(), err, nil).Render(w)
default:
- httperror.InternalError(ctx, fmt.Sprintf("Cannot process instructions for disbursement with ID: %s", disbursementID), err, nil).Render(w)
+ httperror.InternalError(ctx, fmt.Sprintf("Cannot process instructions for disbursement with ID %s", disbursementID), err, nil).Render(w)
}
return
}
@@ -283,6 +278,32 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter,
httpjson.Render(w, response, httpjson.JSON)
}
+// parseCsvFromMultipartRequest parses the CSV file from a multipart request and returns the file content and header,
+// or an error if the file is not a valid CSV or the MIME type is not text/csv.
+func parseCsvFromMultipartRequest(r *http.Request) (*bytes.Buffer, *multipart.FileHeader, *httperror.HTTPError) {
+ // Parse uploaded CSV file
+ file, header, err := r.FormFile("file")
+ if err != nil {
+ return nil, nil, httperror.BadRequest("could not parse file", err, nil)
+ }
+ defer file.Close()
+
+ if err = utils.ValidatePathIsNotTraversal(header.Filename); err != nil {
+ return nil, nil, httperror.BadRequest("file name contains invalid traversal pattern", nil, nil)
+ }
+
+ if filepath.Ext(header.Filename) != ".csv" {
+ return nil, nil, httperror.BadRequest("the file extension should be .csv", nil, nil)
+ }
+
+ var buf bytes.Buffer
+ if _, err = io.Copy(&buf, file); err != nil {
+ return nil, nil, httperror.BadRequest("could not read file", err, nil)
+ }
+
+ return &buf, header, nil
+}
+
func (d DisbursementHandler) GetDisbursement(w http.ResponseWriter, r *http.Request) {
disbursementID := chi.URLParam(r, "id")
@@ -446,8 +467,13 @@ func (d DisbursementHandler) GetDisbursementInstructions(w http.ResponseWriter,
return
}
+ filename := disbursement.FileName
+ if filepath.Ext(filename) != ".csv" { // add .csv extension if missing
+ filename = filename + ".csv"
+ }
+
// `attachment` returns a file-download prompt. change that to `inline` to open in browser
- w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, disbursement.FileName))
+ w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
w.Header().Set("Content-Type", "text/csv")
_, err = w.Write(disbursement.FileContent)
if err != nil {
diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go
index 8ec3aa2ba..4fc86d345 100644
--- a/internal/serve/httphandler/disbursement_handler_test.go
+++ b/internal/serve/httphandler/disbursement_handler_test.go
@@ -840,12 +840,13 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
}
testCases := []struct {
- name string
- disbursementID string
- fieldName string
- csvRecords [][]string
- expectedStatus int
- expectedMessage string
+ name string
+ disbursementID string
+ multipartFieldName string
+ actualFileName string
+ csvRecords [][]string
+ expectedStatus int
+ expectedMessage string
}{
{
name: "valid input",
@@ -857,6 +858,50 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
expectedStatus: http.StatusOK,
expectedMessage: "File uploaded successfully",
},
+ {
+ name: ".bat file fails",
+ disbursementID: draftDisbursement.ID,
+ csvRecords: [][]string{
+ {"phone", "id", "amount", "verification"},
+ {"+380445555555", "123456789", "100.5", "1990-01-01"},
+ },
+ actualFileName: "file.bat",
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: "the file extension should be .csv",
+ },
+ {
+ name: ".sh file fails",
+ disbursementID: draftDisbursement.ID,
+ csvRecords: [][]string{
+ {"phone", "id", "amount", "verification"},
+ {"+380445555555", "123456789", "100.5", "1990-01-01"},
+ },
+ actualFileName: "file.sh",
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: "the file extension should be .csv",
+ },
+ {
+ name: ".bash file fails",
+ disbursementID: draftDisbursement.ID,
+ csvRecords: [][]string{
+ {"phone", "id", "amount", "verification"},
+ {"+380445555555", "123456789", "100.5", "1990-01-01"},
+ },
+ actualFileName: "file.bash",
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: "the file extension should be .csv",
+ },
+ {
+ name: ".csv file with transversal path ..\\.. fails",
+ disbursementID: draftDisbursement.ID,
+ csvRecords: [][]string{
+ {"phone", "id", "amount", "verification"},
+ {"+380445555555", "123456789", "100.5", "1990-01-01"},
+ },
+ actualFileName: "..\\..\\file.csv",
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: "file name contains invalid traversal pattern",
+ },
{
name: "invalid date of birth",
disbursementID: draftDisbursement.ID,
@@ -884,11 +929,11 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
expectedMessage: "disbursement ID is invalid",
},
{
- name: "valid input",
- disbursementID: draftDisbursement.ID,
- fieldName: "instructions",
- expectedStatus: http.StatusBadRequest,
- expectedMessage: "could not parse file",
+ name: "invalid input",
+ disbursementID: draftDisbursement.ID,
+ multipartFieldName: "instructions",
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: "could not parse file",
},
{
name: "disbursement not in draft/ready status",
@@ -903,10 +948,11 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
expectedMessage: "disbursement is not in draft or ready status",
},
{
- name: "error parsing header",
+ name: "error parsing contact type from header",
disbursementID: draftDisbursement.ID,
csvRecords: [][]string{
- {},
+ {"id", "amount", "verification"},
+ {"123456789", "100.5", "1990-01-01"},
},
expectedStatus: http.StatusBadRequest,
expectedMessage: "could not determine contact information type",
@@ -918,32 +964,24 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
{"phone", "id", "amount", "date-of-birth"},
},
expectedStatus: http.StatusBadRequest,
- expectedMessage: "no valid instructions found",
+ expectedMessage: "could not parse csv file",
},
{
name: "instructions invalid - attempting to upload phone and email",
disbursementID: draftDisbursement.ID,
csvRecords: [][]string{
{"phone", "email", "id", "amount", "date-of-birth"},
+ {"+380445555555", "foobar@test.com", "123456789", "100.5", "1990-01-01"},
},
expectedStatus: http.StatusBadRequest,
expectedMessage: "csv file must contain either a phone or email column, not both",
},
- {
- name: "instructions invalid - no phone or email",
- disbursementID: draftDisbursement.ID,
- csvRecords: [][]string{
- {"id", "amount", "date-of-birth"},
- },
- expectedStatus: http.StatusBadRequest,
- expectedMessage: "csv file must contain at least one of the following columns [phone, email]",
- },
{
name: "max instructions exceeded",
disbursementID: draftDisbursement.ID,
csvRecords: maxCSVRecords,
expectedStatus: http.StatusBadRequest,
- expectedMessage: "number of instructions exceeds maximum of : 10000",
+ expectedMessage: "number of instructions exceeds maximum of 10000",
},
}
@@ -952,7 +990,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
fileContent, err := createCSVFile(t, tc.csvRecords)
require.NoError(t, err)
- req, err := createInstructionsMultipartRequest(t, ctx, tc.fieldName, tc.disbursementID, fileContent)
+ req, err := createInstructionsMultipartRequest(t, ctx, tc.multipartFieldName, tc.actualFileName, tc.disbursementID, fileContent)
require.NoError(t, err)
// Record the response
@@ -1624,74 +1662,100 @@ func Test_DisbursementHandler_PatchDisbursementStatus(t *testing.T) {
}
func Test_DisbursementHandler_GetDisbursementInstructions(t *testing.T) {
- ctx := context.Background()
-
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, outerErr := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, outerErr)
defer dbConnectionPool.Close()
+ ctx := context.Background()
models, outerErr := data.NewModels(dbConnectionPool)
require.NoError(t, outerErr)
- handler := &DisbursementHandler{
- Models: models,
- }
-
+ handler := &DisbursementHandler{Models: models}
r := chi.NewRouter()
r.Get("/disbursements/{id}/instructions", handler.GetDisbursementInstructions)
- disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{})
- require.NotNil(t, disbursement)
-
- t.Run("disbursement doesn't exist", func(t *testing.T) {
- id := "9e0ff65f-f6e9-46e9-bf03-dc46723e3bfb"
-
- req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/disbursements/%s/instructions", id), nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- require.Equal(t, http.StatusNotFound, rr.Code)
- require.Contains(t, rr.Body.String(), services.ErrDisbursementNotFound.Error())
+ disbursementFileContent := data.CreateInstructionsFixture(t, []*data.DisbursementInstruction{
+ {Phone: "1234567890", ID: "1", Amount: "123.12", VerificationValue: "1995-02-20"},
+ {Phone: "0987654321", ID: "2", Amount: "321", VerificationValue: "1974-07-19"},
+ {Phone: "0987654321", ID: "3", Amount: "321", VerificationValue: "1974-07-19"},
})
+ d := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{})
+ require.NotNil(t, d)
- t.Run("disbursement has no instructions", func(t *testing.T) {
- req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/disbursements/%s/instructions", disbursement.ID), nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
+ testCases := []struct {
+ name string
+ updateDisbursementFn func(d *data.Disbursement) error
+ getDisbursementIDFn func(d *data.Disbursement) string
+ expectedStatus int
+ expectedErrMessage string
+ wantFilename string
+ }{
+ {
+ name: "404-disbursement doesn't exist",
+ getDisbursementIDFn: func(d *data.Disbursement) string {
+ return "non-existent-disbursement-id"
+ },
+ expectedStatus: http.StatusNotFound,
+ expectedErrMessage: services.ErrDisbursementNotFound.Error(),
+ },
+ {
+ name: "404-disbursement has no instructions",
+ getDisbursementIDFn: func(d *data.Disbursement) string { return d.ID },
+ expectedStatus: http.StatusNotFound,
+ expectedErrMessage: "disbursement " + d.ID + " has no instructions file",
+ },
+ {
+ name: "200-disbursement has instructions",
+ updateDisbursementFn: func(d *data.Disbursement) error {
+ return models.Disbursements.Update(ctx, &data.DisbursementUpdate{
+ ID: d.ID,
+ FileContent: disbursementFileContent,
+ FileName: "instructions.csv",
+ })
+ },
+ wantFilename: "instructions.csv",
+ getDisbursementIDFn: func(d *data.Disbursement) string { return d.ID },
+ expectedStatus: http.StatusOK,
+ },
+ {
+ name: "200-disbursement has instructions but filename is missing .csv",
+ updateDisbursementFn: func(d *data.Disbursement) error {
+ return models.Disbursements.Update(ctx, &data.DisbursementUpdate{
+ ID: d.ID,
+ FileContent: disbursementFileContent,
+ FileName: "instructions.bat",
+ })
+ },
+ wantFilename: "instructions.bat.csv",
+ getDisbursementIDFn: func(d *data.Disbursement) string { return d.ID },
+ expectedStatus: http.StatusOK,
+ },
+ }
- require.Equal(t, http.StatusNotFound, rr.Code)
- require.Contains(t, rr.Body.String(), fmt.Sprintf("disbursement %s has no instructions file", disbursement.ID))
- })
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ if tc.updateDisbursementFn != nil {
+ require.NoError(t, tc.updateDisbursementFn(d))
+ }
- t.Run("disbursement has instructions", func(t *testing.T) {
- disbursementFileContent := data.CreateInstructionsFixture(t, []*data.DisbursementInstruction{
- {Phone: "1234567890", ID: "1", Amount: "123.12", VerificationValue: "1995-02-20"},
- {Phone: "0987654321", ID: "2", Amount: "321", VerificationValue: "1974-07-19"},
- {Phone: "0987654321", ID: "3", Amount: "321", VerificationValue: "1974-07-19"},
- })
+ req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/disbursements/%s/instructions", tc.getDisbursementIDFn(d)), nil)
+ require.NoError(t, err)
+ rr := httptest.NewRecorder()
+ r.ServeHTTP(rr, req)
- err := models.Disbursements.Update(ctx, &data.DisbursementUpdate{
- ID: disbursement.ID,
- FileContent: disbursementFileContent,
- FileName: "instructions.csv",
+ require.Equal(t, tc.expectedStatus, rr.Code)
+ if tc.expectedStatus != http.StatusOK {
+ require.Contains(t, rr.Body.String(), tc.expectedErrMessage)
+ } else {
+ t.Log(rr.Header())
+ require.Equal(t, "text/csv", rr.Header().Get("Content-Type"))
+ require.Equal(t, "attachment; filename=\""+tc.wantFilename+"\"", rr.Header().Get("Content-Disposition"))
+ require.Equal(t, string(disbursementFileContent), rr.Body.String())
+ }
})
- require.NoError(t, err)
-
- req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/disbursements/%s/instructions", disbursement.ID), nil)
- require.NoError(t, err)
- rr := httptest.NewRecorder()
- r.ServeHTTP(rr, req)
-
- require.Equal(t, http.StatusOK, rr.Code)
- require.Equal(t, "text/csv", rr.Header().Get("Content-Type"))
- require.Equal(t, "attachment; filename=\"instructions.csv\"", rr.Header().Get("Content-Disposition"))
- require.Equal(t, string(disbursementFileContent), rr.Body.String())
- })
+ }
}
func createCSVFile(t *testing.T, records [][]string) (io.Reader, error) {
@@ -1705,15 +1769,19 @@ func createCSVFile(t *testing.T, records [][]string) (io.Reader, error) {
return &buf, nil
}
-func createInstructionsMultipartRequest(t *testing.T, ctx context.Context, fieldName, disbursementID string, fileContent io.Reader) (*http.Request, error) {
+func createInstructionsMultipartRequest(t *testing.T, ctx context.Context, multipartFieldName, fileName, disbursementID string, fileContent io.Reader) (*http.Request, error) {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
- if fieldName == "" {
- fieldName = "file"
+ if multipartFieldName == "" {
+ multipartFieldName = "file"
+ }
+
+ if fileName == "" {
+ fileName = "instructions.csv"
}
- part, err := writer.CreateFormFile(fieldName, "instructions.csv")
+ part, err := writer.CreateFormFile(multipartFieldName, fileName)
require.NoError(t, err)
_, err = io.Copy(part, fileContent)
diff --git a/internal/utils/validation.go b/internal/utils/validation.go
index 7e7342571..63d0bf91a 100644
--- a/internal/utils/validation.go
+++ b/internal/utils/validation.go
@@ -1,6 +1,7 @@
package utils
import (
+ "errors"
"fmt"
"regexp"
"strconv"
@@ -162,3 +163,14 @@ func ValidateNationalIDVerification(nationalID string) error {
return nil
}
+
+// ValidatePathIsNotTraversal will validate the given path to ensure it does not contain path traversal.
+func ValidatePathIsNotTraversal(p string) error {
+ if pathTraversalPattern.MatchString(p) {
+ return errors.New("path cannot contain path traversal")
+ }
+
+ return nil
+}
+
+var pathTraversalPattern = regexp.MustCompile(`(^|[\\/])\.\.([\\/]|$)`)
diff --git a/internal/utils/validation_test.go b/internal/utils/validation_test.go
index 517d331a1..f76ec38e2 100644
--- a/internal/utils/validation_test.go
+++ b/internal/utils/validation_test.go
@@ -37,6 +37,37 @@ func Test_ValidatePhoneNumber(t *testing.T) {
}
}
+func Test_ValidatePathIsNotTraversal(t *testing.T) {
+ testCases := []struct {
+ path string
+ isTraversal bool
+ }{
+ {"", false},
+ {"http://example.com", false},
+ {"documents", false},
+ {"./documents/files", false},
+ {"./projects/subproject/report", false},
+ {"http://example.com/../config.yaml", true},
+ {"../config.yaml", true},
+ {"documents/../config.yaml", true},
+ {"docs/files/..", true},
+ {"..\\config.yaml", true},
+ {"documents\\..\\config.yaml", true},
+ {"documents\\files\\..", true},
+ }
+
+ for _, tc := range testCases {
+ t.Run("-"+tc.path, func(t *testing.T) {
+ err := ValidatePathIsNotTraversal(tc.path)
+ if tc.isTraversal {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
func Test_ValidateAmount(t *testing.T) {
testCases := []struct {
amount string
From a5c5e61ce90f381d1f9ea95aeba9b90e720ce4a7 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 29 Oct 2024 13:08:03 -0700
Subject: [PATCH 49/75] [SDP-1361] ensure validation of URLs with the https
schema on pubnet (#445)
### What
Ensure validation of URLs with the https schema on pubnet, and http/https on testnet.
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1261.
---
internal/data/wallets.go | 2 +-
internal/serve/httphandler/profile_handler.go | 14 +
.../serve/httphandler/profile_handler_test.go | 128 +++++--
internal/serve/httphandler/wallets_handler.go | 12 +-
.../serve/httphandler/wallets_handler_test.go | 328 +++++++-----------
internal/serve/serve.go | 6 +-
internal/serve/validators/wallet_validator.go | 27 +-
.../serve/validators/wallet_validator_test.go | 231 ++++++------
internal/utils/network_type.go | 8 +
internal/utils/network_type_test.go | 52 +++
internal/utils/utils.go | 7 +
internal/utils/validation.go | 24 ++
internal/utils/validation_test.go | 39 ++-
13 files changed, 501 insertions(+), 377 deletions(-)
diff --git a/internal/data/wallets.go b/internal/data/wallets.go
index 002a04653..c1f88c5f3 100644
--- a/internal/data/wallets.go
+++ b/internal/data/wallets.go
@@ -161,10 +161,10 @@ func (wm *WalletModel) Insert(ctx context.Context, newWallet WalletInsert) (*Wal
if err != nil {
if pqError, ok := err.(*pq.Error); ok {
constraintErrMap := map[string]error{
+ "wallets_assets_asset_id_fkey": ErrInvalidAssetID,
"wallets_name_key": ErrWalletNameAlreadyExists,
"wallets_homepage_key": ErrWalletHomepageAlreadyExists,
"wallets_deep_link_schema_key": ErrWalletDeepLinkSchemaAlreadyExists,
- "wallets_assets_asset_id_fkey": ErrInvalidAssetID,
}
errConstraint, ok := constraintErrMap[pqError.Constraint]
diff --git a/internal/serve/httphandler/profile_handler.go b/internal/serve/httphandler/profile_handler.go
index 300bd934f..07b334333 100644
--- a/internal/serve/httphandler/profile_handler.go
+++ b/internal/serve/httphandler/profile_handler.go
@@ -47,6 +47,7 @@ type ProfileHandler struct {
PublicFilesFS fs.FS
DistributionAccountResolver signing.DistributionAccountResolver
PasswordValidator *authUtils.PasswordValidator
+ utils.NetworkType
}
type PatchOrganizationProfileRequest struct {
@@ -159,6 +160,19 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
return
}
+ if reqBody.PrivacyPolicyLink != nil && *reqBody.PrivacyPolicyLink != "" {
+ schemes := []string{"https"}
+ if !h.IsPubnet() {
+ schemes = append(schemes, "http")
+ }
+ validator := validators.NewValidator()
+ validator.CheckError(utils.ValidateURLScheme(*reqBody.PrivacyPolicyLink, schemes...), "privacy_policy_link", "")
+ if validator.HasErrors() {
+ httperror.BadRequest("", nil, validator.Errors).Render(rw)
+ return
+ }
+ }
+
organizationUpdate := data.OrganizationUpdate{
Name: reqBody.OrganizationName,
Logo: fileContentBytes,
diff --git a/internal/serve/httphandler/profile_handler_test.go b/internal/serve/httphandler/profile_handler_test.go
index f965d2d7a..09877723f 100644
--- a/internal/serve/httphandler/profile_handler_test.go
+++ b/internal/serve/httphandler/profile_handler_test.go
@@ -32,9 +32,10 @@ import (
"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/publicfiles"
"github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing"
sigMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing/mocks"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
"github.com/stellar/stellar-disbursement-platform-backend/pkg/schema"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/auth"
- "github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/utils"
+ authUtils "github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/utils"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant"
)
@@ -106,10 +107,17 @@ func Test_PatchOrganizationProfileRequest_AreAllFieldsEmpty(t *testing.T) {
}
func Test_ProfileHandler_PatchOrganizationProfile_Failures(t *testing.T) {
+ // Setup DB
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+ dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, err)
+ defer dbConnectionPool.Close()
+
// PNG file
pngImg := data.CreateMockImage(t, 300, 300, data.ImageSizeSmall)
pngImgBuf := new(bytes.Buffer)
- err := png.Encode(pngImgBuf, pngImg)
+ err = png.Encode(pngImgBuf, pngImg)
require.NoError(t, err)
// CSV file
@@ -137,6 +145,7 @@ func Test_ProfileHandler_PatchOrganizationProfile_Failures(t *testing.T) {
mockAuthManagerFn func(authManagerMock *auth.AuthManagerMock)
wantStatusCode int
wantRespBody string
+ networkType utils.NetworkType
}{
{
name: "returns Unauthorized when no token is found",
@@ -221,17 +230,80 @@ func Test_ProfileHandler_PatchOrganizationProfile_Failures(t *testing.T) {
}
}`,
},
+ {
+ name: "returns BadRequest when the privacy_policy_link is invalid",
+ token: "token",
+ mockAuthManagerFn: func(authManagerMock *auth.AuthManagerMock) {
+ authManagerMock.
+ On("GetUser", mock.Anything, "token").
+ Return(user, nil).
+ Once()
+ },
+ getRequestFn: func(t *testing.T, ctx context.Context) *http.Request {
+ reqBody := `{
+ "privacy_policy_link": "example.com/privacy-policy"
+ }`
+ return createOrganizationProfileMultipartRequest(t, ctx, url, "", "", reqBody, new(bytes.Buffer))
+ },
+ wantStatusCode: http.StatusBadRequest,
+ wantRespBody: `{
+ "error": "The request was invalid in some way.",
+ "extras": {
+ "privacy_policy_link": "invalid URL format"
+ }
+ }`,
+ },
+ {
+ name: "returns BadRequest when the privacy_policy_link scheme is invalid",
+ token: "token",
+ mockAuthManagerFn: func(authManagerMock *auth.AuthManagerMock) {
+ authManagerMock.
+ On("GetUser", mock.Anything, "token").
+ Return(user, nil).
+ Once()
+ },
+ getRequestFn: func(t *testing.T, ctx context.Context) *http.Request {
+ reqBody := `{
+ "privacy_policy_link": "ftp://example.com/privacy-policy"
+ }`
+ return createOrganizationProfileMultipartRequest(t, ctx, url, "", "", reqBody, new(bytes.Buffer))
+ },
+ wantStatusCode: http.StatusBadRequest,
+ wantRespBody: `{
+ "error": "The request was invalid in some way.",
+ "extras": {
+ "privacy_policy_link": "invalid URL scheme is not part of [https http]"
+ }
+ }`,
+ },
+ {
+ name: "returns BadRequest when the privacy_policy_link scheme is invalid (pubnet)",
+ token: "token",
+ mockAuthManagerFn: func(authManagerMock *auth.AuthManagerMock) {
+ authManagerMock.
+ On("GetUser", mock.Anything, "token").
+ Return(user, nil).
+ Once()
+ },
+ getRequestFn: func(t *testing.T, ctx context.Context) *http.Request {
+ reqBody := `{
+ "privacy_policy_link": "http://example.com/privacy-policy"
+ }`
+ return createOrganizationProfileMultipartRequest(t, ctx, url, "", "", reqBody, new(bytes.Buffer))
+ },
+ networkType: utils.PubnetNetworkType,
+ wantStatusCode: http.StatusBadRequest,
+ wantRespBody: `{
+ "error": "The request was invalid in some way.",
+ "extras": {
+ "privacy_policy_link": "invalid URL scheme is not part of [https]"
+ }
+ }`,
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- // Setup DB
- dbt := dbtest.Open(t)
- defer dbt.Close()
- dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
- require.NoError(t, err)
- defer dbConnectionPool.Close()
-
// Inject authenticated token into context:
ctx := context.Background()
if tc.token != "" {
@@ -239,11 +311,15 @@ func Test_ProfileHandler_PatchOrganizationProfile_Failures(t *testing.T) {
}
// Setup password validator
- pwValidator, err := utils.GetPasswordValidatorInstance()
+ pwValidator, err := authUtils.GetPasswordValidatorInstance()
require.NoError(t, err)
// Setup handler with mocked dependencies
- handler := &ProfileHandler{MaxMemoryAllocation: 1024 * 1024, PasswordValidator: pwValidator}
+ handler := &ProfileHandler{
+ MaxMemoryAllocation: 1024 * 1024,
+ PasswordValidator: pwValidator,
+ NetworkType: tc.networkType,
+ }
if tc.mockAuthManagerFn != nil {
authManagerMock := &auth.AuthManagerMock{}
tc.mockAuthManagerFn(authManagerMock)
@@ -268,12 +344,21 @@ func Test_ProfileHandler_PatchOrganizationProfile_Failures(t *testing.T) {
}
func Test_ProfileHandler_PatchOrganizationProfile_Successful(t *testing.T) {
+ // Setup DB
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+ dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, err)
+ defer dbConnectionPool.Close()
+ models, err := data.NewModels(dbConnectionPool)
+ require.NoError(t, err)
+
// PNG file
newPNGImgBuf := func() *bytes.Buffer {
pngImg := data.CreateMockImage(t, 300, 300, data.ImageSizeSmall)
pngImgBuf := new(bytes.Buffer)
- err := png.Encode(pngImgBuf, pngImg)
- require.NoError(t, err)
+ innerErr := png.Encode(pngImgBuf, pngImg)
+ require.NoError(t, innerErr)
return pngImgBuf
}
@@ -283,7 +368,7 @@ func Test_ProfileHandler_PatchOrganizationProfile_Successful(t *testing.T) {
// JPEG file
jpegImg := data.CreateMockImage(t, 300, 300, data.ImageSizeSmall)
jpegImgBuf := new(bytes.Buffer)
- err := jpeg.Encode(jpegImgBuf, jpegImg, &jpeg.Options{Quality: jpeg.DefaultQuality})
+ err = jpeg.Encode(jpegImgBuf, jpegImg, &jpeg.Options{Quality: jpeg.DefaultQuality})
require.NoError(t, err)
url := "/profile/organization"
@@ -417,15 +502,6 @@ func Test_ProfileHandler_PatchOrganizationProfile_Successful(t *testing.T) {
log.DefaultLogger.SetOutput(buf)
log.SetLevel(log.InfoLevel)
- // Setup DB
- dbt := dbtest.Open(t)
- defer dbt.Close()
- dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
- require.NoError(t, err)
- defer dbConnectionPool.Close()
- models, err := data.NewModels(dbConnectionPool)
- require.NoError(t, err)
-
// Inject authenticated token into context:
ctx := context.Background()
if tc.token != "" {
@@ -447,7 +523,7 @@ func Test_ProfileHandler_PatchOrganizationProfile_Successful(t *testing.T) {
}
// Setup password validator
- pwValidator, err := utils.GetPasswordValidatorInstance()
+ pwValidator, err := authUtils.GetPasswordValidatorInstance()
require.NoError(t, err)
// Setup handler with mocked dependencies
@@ -621,7 +697,7 @@ func Test_ProfileHandler_PatchUserProfile(t *testing.T) {
}
// Setup password validator
- pwValidator, err := utils.GetPasswordValidatorInstance()
+ pwValidator, err := authUtils.GetPasswordValidatorInstance()
require.NoError(t, err)
// Setup handler with mocked dependencies
@@ -802,7 +878,7 @@ func Test_ProfileHandler_PatchUserPassword(t *testing.T) {
}
// Setup password validator
- pwValidator, err := utils.GetPasswordValidatorInstance()
+ pwValidator, err := authUtils.GetPasswordValidatorInstance()
require.NoError(t, err)
// Setup handler with mocked dependencies
diff --git a/internal/serve/httphandler/wallets_handler.go b/internal/serve/httphandler/wallets_handler.go
index 28d925c32..cd2cc8c93 100644
--- a/internal/serve/httphandler/wallets_handler.go
+++ b/internal/serve/httphandler/wallets_handler.go
@@ -13,10 +13,12 @@ import (
"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror"
"github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
type WalletsHandler struct {
- Models *data.Models
+ Models *data.Models
+ NetworkType utils.NetworkType
}
// GetWallets returns a list of wallets
@@ -52,7 +54,7 @@ func (h WalletsHandler) PostWallets(rw http.ResponseWriter, req *http.Request) {
}
validator := validators.NewWalletValidator()
- reqBody = validator.ValidateCreateWalletRequest(ctx, reqBody)
+ reqBody = validator.ValidateCreateWalletRequest(ctx, reqBody, h.NetworkType.IsPubnet())
if validator.HasErrors() {
httperror.BadRequest("invalid request body", nil, validator.Errors).Render(rw)
return
@@ -67,6 +69,9 @@ func (h WalletsHandler) PostWallets(rw http.ResponseWriter, req *http.Request) {
})
if err != nil {
switch {
+ case errors.Is(err, data.ErrInvalidAssetID):
+ httperror.BadRequest(data.ErrInvalidAssetID.Error(), err, nil).Render(rw)
+ return
case errors.Is(err, data.ErrWalletNameAlreadyExists):
httperror.Conflict(data.ErrWalletNameAlreadyExists.Error(), err, nil).Render(rw)
return
@@ -76,9 +81,6 @@ func (h WalletsHandler) PostWallets(rw http.ResponseWriter, req *http.Request) {
case errors.Is(err, data.ErrWalletDeepLinkSchemaAlreadyExists):
httperror.Conflict(data.ErrWalletDeepLinkSchemaAlreadyExists.Error(), err, nil).Render(rw)
return
- case errors.Is(err, data.ErrInvalidAssetID):
- httperror.Conflict(data.ErrInvalidAssetID.Error(), err, nil).Render(rw)
- return
}
httperror.InternalError(ctx, "", err, nil).Render(rw)
diff --git a/internal/serve/httphandler/wallets_handler_test.go b/internal/serve/httphandler/wallets_handler_test.go
index 3ca54842c..5e729b9fa 100644
--- a/internal/serve/httphandler/wallets_handler_test.go
+++ b/internal/serve/httphandler/wallets_handler_test.go
@@ -129,7 +129,6 @@ func Test_WalletsHandlerGetWallets(t *testing.T) {
func Test_WalletsHandlerPostWallets(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -138,44 +137,30 @@ func Test_WalletsHandlerPostWallets(t *testing.T) {
require.NoError(t, err)
ctx := context.Background()
+ handler := &WalletsHandler{Models: models}
- handler := &WalletsHandler{
- Models: models,
- }
-
- data.DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
+ // Fixture setup
+ wallet := data.ClearAndCreateWalletFixtures(t, ctx, dbConnectionPool)[0]
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "XLM", "")
- t.Run("returns BadRequest when payload is invalid", func(t *testing.T) {
- rr := httptest.NewRecorder()
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(`invalid`))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp := rr.Result()
-
- respBody, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
- assert.JSONEq(t, `{"error": "The request was invalid in some way."}`, string(respBody))
-
- rr = httptest.NewRecorder()
- req, err = http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(`{}`))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp = rr.Result()
-
- respBody, err = io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- expected := `
- {
+ // Define test cases
+ testCases := []struct {
+ name string
+ payload string
+ expectedStatus int
+ expectedBody string
+ }{
+ {
+ name: "๐ด-400-BadRequest when payload is invalid",
+ payload: `invalid`,
+ expectedStatus: http.StatusBadRequest,
+ expectedBody: `{"error": "The request was invalid in some way."}`,
+ },
+ {
+ name: "๐ด-400-BadRequest when payload is missing required fields",
+ payload: `{}`,
+ expectedStatus: http.StatusBadRequest,
+ expectedBody: `{
"error": "invalid request body",
"extras": {
"name": "name is required",
@@ -184,215 +169,134 @@ func Test_WalletsHandlerPostWallets(t *testing.T) {
"sep_10_client_domain": "sep_10_client_domain is required",
"assets_ids": "provide at least one asset ID"
}
- }
- `
- assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
- assert.JSONEq(t, expected, string(respBody))
-
- payload := `
- {
+ }`,
+ },
+ {
+ name: "๐ด-400-BadRequest when assets_ids is missing",
+ payload: `{
"name": "New Wallet",
"homepage": "https://newwallet.com",
"deep_link_schema": "newwallet://sdp",
"sep_10_client_domain": "https://newwallet.com"
- }
- `
- rr = httptest.NewRecorder()
- req, err = http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(payload))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp = rr.Result()
-
- respBody, err = io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- expected = `
- {
+ }`,
+ expectedStatus: http.StatusBadRequest,
+ expectedBody: `{
"error": "invalid request body",
"extras": {
"assets_ids": "provide at least one asset ID"
}
- }
- `
- assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
- assert.JSONEq(t, expected, string(respBody))
- })
-
- t.Run("returns BadRequest when the URLs are invalids", func(t *testing.T) {
- payload := fmt.Sprintf(`
- {
+ }`,
+ },
+ {
+ name: "๐ด-400-BadRequest when URLs are invalid",
+ payload: fmt.Sprintf(`{
"name": "New Wallet",
"homepage": "newwallet.com",
"deep_link_schema": "deeplink/sdp",
"sep_10_client_domain": "https://newwallet.com",
"assets_ids": [%q]
- }
- `, asset.ID)
- rr := httptest.NewRecorder()
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(payload))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp := rr.Result()
-
- respBody, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- expected := `
- {
+ }`, asset.ID),
+ expectedStatus: http.StatusBadRequest,
+ expectedBody: `{
"error": "invalid request body",
"extras": {
"deep_link_schema": "invalid deep link schema provided",
"homepage": "invalid homepage URL provided"
}
- }
- `
- assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
- assert.JSONEq(t, expected, string(respBody))
- })
-
- t.Run("returns Conflict when creating a duplicated wallet", func(t *testing.T) {
- wallet := data.ClearAndCreateWalletFixtures(t, ctx, dbConnectionPool)[0]
-
- // Duplicated Name
- payload := fmt.Sprintf(`
- {
+ }`,
+ },
+ {
+ name: "๐ด-400-BadRequest when creating a wallet with an invalid asset ID",
+ payload: `{
+ "name": "New Wallet",
+ "homepage": "https://newwallet.com",
+ "deep_link_schema": "newwallet://sdp",
+ "sep_10_client_domain": "https://newwallet.com",
+ "assets_ids": ["invalid-asset-id"]
+ }`,
+ expectedStatus: http.StatusBadRequest,
+ expectedBody: `{"error": "invalid asset ID"}`,
+ },
+ {
+ name: "๐ด-409-Conflict when creating a duplicated wallet (name)",
+ payload: fmt.Sprintf(`{
"name": %q,
- "homepage": %q,
- "deep_link_schema": %q,
- "sep_10_client_domain": %q,
+ "homepage": "https://newwallet.com",
+ "deep_link_schema": "newwallet://sdp",
+ "sep_10_client_domain": "https://newwallet.com",
"assets_ids": [%q]
- }
- `, wallet.Name, wallet.Homepage, wallet.DeepLinkSchema, wallet.SEP10ClientDomain, asset.ID)
- rr := httptest.NewRecorder()
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(payload))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp := rr.Result()
-
- respBody, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- assert.Equal(t, http.StatusConflict, resp.StatusCode)
- assert.JSONEq(t, `{"error": "a wallet with this name already exists"}`, string(respBody))
-
- // Duplicated Homepage
- payload = fmt.Sprintf(`
- {
+ }`, wallet.Name, asset.ID),
+ expectedStatus: http.StatusConflict,
+ expectedBody: `{"error": "a wallet with this name already exists"}`,
+ },
+ {
+ name: "๐ด-409-Conflict when creating a duplicated wallet (homepage)",
+ payload: fmt.Sprintf(`{
"name": "New Wallet",
"homepage": %q,
- "deep_link_schema": %q,
- "sep_10_client_domain": %q,
+ "deep_link_schema": "newwallet://sdp",
+ "sep_10_client_domain": "https://newwallet.com",
"assets_ids": [%q]
- }
- `, wallet.Homepage, wallet.DeepLinkSchema, wallet.SEP10ClientDomain, asset.ID)
- rr = httptest.NewRecorder()
- req, err = http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(payload))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp = rr.Result()
-
- respBody, err = io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- assert.Equal(t, http.StatusConflict, resp.StatusCode)
- assert.JSONEq(t, `{"error": "a wallet with this homepage already exists"}`, string(respBody))
-
- // Duplicated Deep Link Schema
- payload = fmt.Sprintf(`
- {
+ }`, wallet.Homepage, asset.ID),
+ expectedStatus: http.StatusConflict,
+ expectedBody: `{"error": "a wallet with this homepage already exists"}`,
+ },
+ {
+ name: "๐ด-409-Conflict when creating a duplicated wallet (deep_link_schema)",
+ payload: fmt.Sprintf(`{
"name": "New Wallet",
"homepage": "https://newwallet.com",
"deep_link_schema": %q,
- "sep_10_client_domain": %q,
+ "sep_10_client_domain": "https://newwallet.com",
"assets_ids": [%q]
- }
- `, wallet.DeepLinkSchema, wallet.SEP10ClientDomain, asset.ID)
- rr = httptest.NewRecorder()
- req, err = http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(payload))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp = rr.Result()
-
- respBody, err = io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- assert.Equal(t, http.StatusConflict, resp.StatusCode)
- assert.JSONEq(t, `{"error": "a wallet with this deep link schema already exists"}`, string(respBody))
-
- // Invalid asset ID
- payload = fmt.Sprintf(`
- {
- "name": "New Wallet",
- "homepage": "https://newwallet.com",
- "deep_link_schema": "newwallet://sdp",
- "sep_10_client_domain": %q,
- "assets_ids": ["asset-id"]
- }
- `, wallet.SEP10ClientDomain)
- rr = httptest.NewRecorder()
- req, err = http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(payload))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp = rr.Result()
-
- respBody, err = io.ReadAll(resp.Body)
- require.NoError(t, err)
- defer resp.Body.Close()
-
- assert.Equal(t, http.StatusConflict, resp.StatusCode)
- assert.JSONEq(t, `{"error": "invalid asset ID"}`, string(respBody))
- })
-
- t.Run("creates wallet successfully", func(t *testing.T) {
- data.DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
-
- payload := fmt.Sprintf(`
- {
+ }`, wallet.DeepLinkSchema, asset.ID),
+ expectedStatus: http.StatusConflict,
+ expectedBody: `{"error": "a wallet with this deep link schema already exists"}`,
+ },
+ {
+ name: "๐ข-successfully creates wallet",
+ payload: fmt.Sprintf(`{
"name": "New Wallet",
"homepage": "https://newwallet.com",
"deep_link_schema": "newwallet://deeplink/sdp",
"sep_10_client_domain": "https://newwallet.com",
"assets_ids": [%q]
- }
- `, asset.ID)
- rr := httptest.NewRecorder()
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(payload))
- require.NoError(t, err)
-
- http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
-
- resp := rr.Result()
-
- assert.Equal(t, http.StatusCreated, resp.StatusCode)
-
- wallet, err := models.Wallets.GetByWalletName(ctx, "New Wallet")
- require.NoError(t, err)
-
- walletAssets, err := models.Wallets.GetAssets(ctx, wallet.ID)
- require.NoError(t, err)
+ }`, asset.ID),
+ expectedStatus: http.StatusCreated,
+ expectedBody: "",
+ },
+ }
- assert.Equal(t, "https://newwallet.com", wallet.Homepage)
- assert.Equal(t, "newwallet://deeplink/sdp", wallet.DeepLinkSchema)
- assert.Equal(t, "newwallet.com", wallet.SEP10ClientDomain)
- assert.Len(t, walletAssets, 1)
- })
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ rr := httptest.NewRecorder()
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/wallets", strings.NewReader(tc.payload))
+ require.NoError(t, err)
+
+ http.HandlerFunc(handler.PostWallets).ServeHTTP(rr, req)
+
+ resp := rr.Result()
+ defer resp.Body.Close()
+ respBody, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ assert.Equal(t, tc.expectedStatus, resp.StatusCode)
+ if tc.expectedBody != "" {
+ assert.JSONEq(t, tc.expectedBody, string(respBody))
+ } else if tc.expectedStatus == http.StatusCreated {
+ wallet, err := models.Wallets.GetByWalletName(ctx, "New Wallet")
+ require.NoError(t, err)
+
+ walletAssets, err := models.Wallets.GetAssets(ctx, wallet.ID)
+ require.NoError(t, err)
+
+ assert.Equal(t, "https://newwallet.com", wallet.Homepage)
+ assert.Equal(t, "newwallet://deeplink/sdp", wallet.DeepLinkSchema)
+ assert.Equal(t, "newwallet.com", wallet.SEP10ClientDomain)
+ assert.Len(t, walletAssets, 1)
+ }
+ })
+ }
}
func Test_WalletsHandlerDeleteWallet(t *testing.T) {
diff --git a/internal/serve/serve.go b/internal/serve/serve.go
index 48296a1a1..ab905f053 100644
--- a/internal/serve/serve.go
+++ b/internal/serve/serve.go
@@ -346,7 +346,10 @@ func handleHTTP(o ServeOptions) *chi.Mux {
})
r.With(middleware.AnyRoleMiddleware(authManager, data.GetAllRoles()...)).Route("/wallets", func(r chi.Router) {
- walletsHandler := httphandler.WalletsHandler{Models: o.Models}
+ walletsHandler := httphandler.WalletsHandler{
+ Models: o.Models,
+ NetworkType: o.NetworkType,
+ }
r.Get("/", walletsHandler.GetWallets)
r.With(middleware.AnyRoleMiddleware(authManager, data.DeveloperUserRole)).
Post("/", walletsHandler.PostWallets)
@@ -364,6 +367,7 @@ func handleHTTP(o ServeOptions) *chi.Mux {
DistributionAccountResolver: o.SubmitterEngine.DistributionAccountResolver,
PasswordValidator: o.PasswordValidator,
PublicFilesFS: publicfiles.PublicFiles,
+ NetworkType: o.NetworkType,
}
r.Route("/profile", func(r chi.Router) {
r.With(middleware.AnyRoleMiddleware(authManager, data.GetAllRoles()...)).
diff --git a/internal/serve/validators/wallet_validator.go b/internal/serve/validators/wallet_validator.go
index 570050660..dcd34239d 100644
--- a/internal/serve/validators/wallet_validator.go
+++ b/internal/serve/validators/wallet_validator.go
@@ -6,6 +6,8 @@ import (
"strings"
"github.com/stellar/go/support/log"
+
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
type WalletRequest struct {
@@ -28,13 +30,14 @@ func NewWalletValidator() *WalletValidator {
return &WalletValidator{Validator: NewValidator()}
}
-func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqBody *WalletRequest) *WalletRequest {
+func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqBody *WalletRequest, enforceHTTPS bool) *WalletRequest {
+ // empty body validation
wv.Check(reqBody != nil, "body", "request body is empty")
-
if wv.HasErrors() {
return nil
}
+ // empty fields validation
name := strings.TrimSpace(reqBody.Name)
homepage := strings.TrimSpace(reqBody.Homepage)
deepLinkSchema := strings.TrimSpace(reqBody.DeepLinkSchema)
@@ -45,15 +48,21 @@ func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqB
wv.Check(deepLinkSchema != "", "deep_link_schema", "deep_link_schema is required")
wv.Check(sep10ClientDomain != "", "sep_10_client_domain", "sep_10_client_domain is required")
wv.Check(len(reqBody.AssetsIDs) != 0, "assets_ids", "provide at least one asset ID")
-
if wv.HasErrors() {
return nil
}
+ // fields format validation
homepageURL, err := url.ParseRequestURI(homepage)
if err != nil {
log.Ctx(ctx).Errorf("parsing homepage URL: %v", err)
wv.Check(false, "homepage", "invalid homepage URL provided")
+ } else {
+ schemes := []string{"https"}
+ if !enforceHTTPS {
+ schemes = append(schemes, "http")
+ }
+ wv.CheckError(utils.ValidateURLScheme(homepage, schemes...), "homepage", "")
}
deepLinkSchemaURL, err := url.ParseRequestURI(deepLinkSchema)
@@ -68,14 +77,18 @@ func (wv *WalletValidator) ValidateCreateWalletRequest(ctx context.Context, reqB
wv.Check(false, "sep_10_client_domain", "invalid SEP-10 client domain URL provided")
}
- if wv.HasErrors() {
- return nil
- }
-
sep10Host := sep10URL.Host
if sep10Host == "" {
sep10Host = sep10URL.String()
}
+ if err := utils.ValidateDNS(sep10Host); err != nil {
+ log.Ctx(ctx).Errorf("validating SEP-10 client domain: %v", err)
+ wv.Check(false, "sep_10_client_domain", "invalid SEP-10 client domain provided")
+ }
+
+ if wv.HasErrors() {
+ return nil
+ }
modifiedReq := &WalletRequest{
Name: name,
diff --git a/internal/serve/validators/wallet_validator_test.go b/internal/serve/validators/wallet_validator_test.go
index f6ba31644..fcaf71201 100644
--- a/internal/serve/validators/wallet_validator_test.go
+++ b/internal/serve/validators/wallet_validator_test.go
@@ -4,7 +4,6 @@ import (
"context"
"testing"
- "github.com/stellar/go/support/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -12,128 +11,112 @@ import (
func TestWalletValidator_ValidateCreateWalletRequest(t *testing.T) {
ctx := context.Background()
- t.Run("returns error when request body is empty", func(t *testing.T) {
- wv := NewWalletValidator()
- wv.ValidateCreateWalletRequest(ctx, nil)
- assert.True(t, wv.HasErrors())
- assert.Equal(t, map[string]interface{}{"body": "request body is empty"}, wv.Errors)
- })
-
- t.Run("returns error when request body has empty fields", func(t *testing.T) {
- wv := NewWalletValidator()
- reqBody := &WalletRequest{}
-
- wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.True(t, wv.HasErrors())
- assert.Equal(t, map[string]interface{}{
- "deep_link_schema": "deep_link_schema is required",
- "homepage": "homepage is required",
- "name": "name is required",
- "sep_10_client_domain": "sep_10_client_domain is required",
- "assets_ids": "provide at least one asset ID",
- }, wv.Errors)
-
- reqBody.Name = "Wallet Provider"
- wv.Errors = map[string]interface{}{}
- wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.True(t, wv.HasErrors())
- assert.Equal(t, map[string]interface{}{
- "deep_link_schema": "deep_link_schema is required",
- "homepage": "homepage is required",
- "sep_10_client_domain": "sep_10_client_domain is required",
- "assets_ids": "provide at least one asset ID",
- }, wv.Errors)
- })
-
- t.Run("returns error when homepage/deep link schema has a invalid URL", func(t *testing.T) {
- getEntries := log.DefaultLogger.StartTest(log.ErrorLevel)
-
- wv := NewWalletValidator()
- reqBody := &WalletRequest{
- Name: "Wallet Provider",
- Homepage: "no-schema-homepage.com",
- DeepLinkSchema: "no-schema-deep-link",
- SEP10ClientDomain: "sep-10-client-domain.com",
- AssetsIDs: []string{"asset-id"},
- }
-
- wv.ValidateCreateWalletRequest(ctx, reqBody)
-
- assert.True(t, wv.HasErrors())
-
- assert.Contains(t, wv.Errors, "homepage")
- assert.Equal(t, "invalid homepage URL provided", wv.Errors["homepage"])
-
- assert.Contains(t, wv.Errors, "deep_link_schema")
- assert.Equal(t, "invalid deep link schema provided", wv.Errors["deep_link_schema"])
-
- entries := getEntries()
- require.Len(t, entries, 2)
- assert.Equal(t, `parsing homepage URL: parse "no-schema-homepage.com": invalid URI for request`, entries[0].Message)
- assert.Equal(t, `parsing deep link schema: parse "no-schema-deep-link": invalid URI for request`, entries[1].Message)
- })
-
- t.Run("validates the homepage successfully", func(t *testing.T) {
- wv := NewWalletValidator()
- reqBody := &WalletRequest{
- Name: "Wallet Provider",
- Homepage: "https://homepage.com",
- DeepLinkSchema: "wallet://deeplinkschema/sdp",
- SEP10ClientDomain: "sep-10-client-domain.com",
- AssetsIDs: []string{"asset-id"},
- }
-
- wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.False(t, wv.HasErrors())
-
- reqBody.Homepage = "http://homepage.com/sdp?redirect=true"
- wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.False(t, wv.HasErrors())
- assert.Equal(t, map[string]interface{}{}, wv.Errors)
- })
-
- t.Run("validates the deep link schema successfully", func(t *testing.T) {
- wv := NewWalletValidator()
- reqBody := &WalletRequest{
- Name: "Wallet Provider",
- Homepage: "https://homepage.com",
- DeepLinkSchema: "wallet://deeplinkschema/sdp",
- SEP10ClientDomain: "sep-10-client-domain.com",
- AssetsIDs: []string{"asset-id"},
- }
-
- wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.False(t, wv.HasErrors())
-
- reqBody.DeepLinkSchema = "https://deeplinkschema.com/sdp?redirect=true"
- wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.False(t, wv.HasErrors())
- })
-
- t.Run("validates the SEP-10 Client Domain successfully", func(t *testing.T) {
- wv := NewWalletValidator()
- reqBody := &WalletRequest{
- Name: "Wallet Provider",
- Homepage: "https://homepage.com",
- DeepLinkSchema: "wallet://deeplinkschema/sdp",
- SEP10ClientDomain: "https://sep-10-client-domain.com",
- AssetsIDs: []string{"asset-id"},
- }
-
- reqBody = wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.False(t, wv.HasErrors())
- assert.Equal(t, "sep-10-client-domain.com", reqBody.SEP10ClientDomain)
-
- reqBody.SEP10ClientDomain = "https://sep-10-client-domain.com/sdp?redirect=true"
- reqBody = wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.False(t, wv.HasErrors())
- assert.Equal(t, "sep-10-client-domain.com", reqBody.SEP10ClientDomain)
-
- reqBody.SEP10ClientDomain = "http://localhost:8000"
- reqBody = wv.ValidateCreateWalletRequest(ctx, reqBody)
- assert.False(t, wv.HasErrors())
- assert.Equal(t, "localhost:8000", reqBody.SEP10ClientDomain)
- })
+ testCases := []struct {
+ name string
+ reqBody *WalletRequest
+ expectedErrs map[string]interface{}
+ updateRequestFn func(wr *WalletRequest)
+ enforceHTTPS bool
+ }{
+ {
+ name: "๐ด error when request body is empty",
+ reqBody: nil,
+ expectedErrs: map[string]interface{}{"body": "request body is empty"},
+ },
+ {
+ name: "๐ด error when request body has empty fields",
+ reqBody: &WalletRequest{},
+ expectedErrs: map[string]interface{}{
+ "deep_link_schema": "deep_link_schema is required",
+ "homepage": "homepage is required",
+ "name": "name is required",
+ "sep_10_client_domain": "sep_10_client_domain is required",
+ "assets_ids": "provide at least one asset ID",
+ },
+ },
+ {
+ name: "๐ด error when homepage,deep-link,client-domain are invalid",
+ reqBody: &WalletRequest{
+ Name: "Wallet Provider",
+ Homepage: "no-schema-homepage.com",
+ DeepLinkSchema: "no-schema-deep-link",
+ SEP10ClientDomain: "-invaliddomain",
+ AssetsIDs: []string{"asset-id"},
+ },
+ expectedErrs: map[string]interface{}{
+ "homepage": "invalid homepage URL provided",
+ "deep_link_schema": "invalid deep link schema provided",
+ "sep_10_client_domain": "invalid SEP-10 client domain provided",
+ },
+ },
+ {
+ name: "๐ข successfully validates the homepage,deep-link,client-domain",
+ reqBody: &WalletRequest{
+ Name: "Wallet Provider",
+ Homepage: "https://homepage.com",
+ DeepLinkSchema: "wallet://deeplinkschema/sdp",
+ SEP10ClientDomain: "sep-10-client-domain.com",
+ AssetsIDs: []string{"asset-id"},
+ },
+ expectedErrs: map[string]interface{}{},
+ },
+ {
+ name: "๐ข successfully validates the homepage,deep-link,client-domain with query params",
+ reqBody: &WalletRequest{
+ Name: "Wallet Provider",
+ Homepage: "http://homepage.com/sdp?redirect=true",
+ DeepLinkSchema: "https://deeplinkschema.com/sdp?redirect=true",
+ SEP10ClientDomain: "sep-10-client-domain.com",
+ AssetsIDs: []string{"asset-id"},
+ },
+ expectedErrs: map[string]interface{}{},
+ },
+ {
+ name: "๐ด fails if enforceHttps=true && homepage=http://...",
+ reqBody: &WalletRequest{
+ Name: "Wallet Provider",
+ Homepage: "http://homepage.com/sdp?redirect=true",
+ DeepLinkSchema: "https://deeplinkschema.com/sdp?redirect=true",
+ SEP10ClientDomain: "sep-10-client-domain.com",
+ AssetsIDs: []string{"asset-id"},
+ },
+ expectedErrs: map[string]interface{}{
+ "homepage": "invalid URL scheme is not part of [https]",
+ },
+ enforceHTTPS: true,
+ },
+ {
+ name: "๐ข successfully validates the homepage,deep-link,client-domain and values get sanitized",
+ reqBody: &WalletRequest{
+ Name: "Wallet Provider",
+ Homepage: "https://homepage.com",
+ DeepLinkSchema: "wallet://deeplinkschema/sdp",
+ SEP10ClientDomain: "https://sep-10-client-domain.com",
+ AssetsIDs: []string{"asset-id"},
+ },
+ updateRequestFn: func(wr *WalletRequest) {
+ wr.SEP10ClientDomain = "sep-10-client-domain.com"
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ wv := NewWalletValidator()
+ reqBody := wv.ValidateCreateWalletRequest(ctx, tc.reqBody, tc.enforceHTTPS)
+
+ if len(tc.expectedErrs) == 0 {
+ require.Falsef(t, wv.HasErrors(), "expected no errors, got: %v", wv.Errors)
+ if tc.updateRequestFn != nil {
+ tc.updateRequestFn(tc.reqBody)
+ }
+ assert.Equal(t, tc.reqBody, reqBody)
+ } else {
+ assert.True(t, wv.HasErrors())
+ assert.Equal(t, tc.expectedErrs, wv.Errors)
+ }
+ })
+ }
}
func TestWalletValidator_ValidatePatchWalletRequest(t *testing.T) {
@@ -141,7 +124,7 @@ func TestWalletValidator_ValidatePatchWalletRequest(t *testing.T) {
t.Run("returns error when request body is empty", func(t *testing.T) {
wv := NewWalletValidator()
- wv.ValidateCreateWalletRequest(ctx, nil)
+ wv.ValidateCreateWalletRequest(ctx, nil, false)
assert.True(t, wv.HasErrors())
assert.Equal(t, map[string]interface{}{"body": "request body is empty"}, wv.Errors)
})
diff --git a/internal/utils/network_type.go b/internal/utils/network_type.go
index 86bd3de5d..428bbf881 100644
--- a/internal/utils/network_type.go
+++ b/internal/utils/network_type.go
@@ -28,6 +28,14 @@ func (n NetworkType) Validate() error {
return nil
}
+func (n NetworkType) IsPubnet() bool {
+ return n == PubnetNetworkType
+}
+
+func (n NetworkType) IsTestnet() bool {
+ return n == TestnetNetworkType
+}
+
func GetNetworkTypeFromNetworkPassphrase(networkPassphrase string) (NetworkType, error) {
switch networkPassphrase {
case network.PublicNetworkPassphrase:
diff --git a/internal/utils/network_type_test.go b/internal/utils/network_type_test.go
index c7b8b20dd..e2f21b739 100644
--- a/internal/utils/network_type_test.go
+++ b/internal/utils/network_type_test.go
@@ -44,6 +44,58 @@ func Test_NetworkType_Validate(t *testing.T) {
}
}
+func Test_NetworkType_IsTestnet(t *testing.T) {
+ testCases := []struct {
+ networkType NetworkType
+ expectedResult bool
+ }{
+ {
+ networkType: TestnetNetworkType,
+ expectedResult: true,
+ },
+ {
+ networkType: PubnetNetworkType,
+ expectedResult: false,
+ },
+ {
+ networkType: "UNSUPPORTED",
+ expectedResult: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(string(tc.networkType), func(t *testing.T) {
+ assert.Equal(t, tc.expectedResult, tc.networkType.IsTestnet())
+ })
+ }
+}
+
+func Test_NetworkType_IsPubnet(t *testing.T) {
+ testCases := []struct {
+ networkType NetworkType
+ expectedResult bool
+ }{
+ {
+ networkType: TestnetNetworkType,
+ expectedResult: false,
+ },
+ {
+ networkType: PubnetNetworkType,
+ expectedResult: true,
+ },
+ {
+ networkType: "UNSUPPORTED",
+ expectedResult: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(string(tc.networkType), func(t *testing.T) {
+ assert.Equal(t, tc.expectedResult, tc.networkType.IsPubnet())
+ })
+ }
+}
+
func Test_GetNetworkTypeFromNetworkPassphrase(t *testing.T) {
testCases := []struct {
networkPassphrase string
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
index 35211733d..6334a1ce9 100644
--- a/internal/utils/utils.go
+++ b/internal/utils/utils.go
@@ -103,3 +103,10 @@ func IntPtr(i int) *int {
func TimePtr(t time.Time) *time.Time {
return &t
}
+
+func VisualBool(b bool) string {
+ if b {
+ return "๐ข"
+ }
+ return "๐ด"
+}
diff --git a/internal/utils/validation.go b/internal/utils/validation.go
index 63d0bf91a..b7ef60d0c 100644
--- a/internal/utils/validation.go
+++ b/internal/utils/validation.go
@@ -3,7 +3,9 @@ package utils
import (
"errors"
"fmt"
+ "net/url"
"regexp"
+ "slices"
"strconv"
"time"
@@ -174,3 +176,25 @@ func ValidatePathIsNotTraversal(p string) error {
}
var pathTraversalPattern = regexp.MustCompile(`(^|[\\/])\.\.([\\/]|$)`)
+
+// ValidateURLScheme checks if a URL is valid and if it has a valid scheme.
+func ValidateURLScheme(link string, scheme ...string) error {
+ // Use govalidator to check if it's a valid URL
+ if !govalidator.IsURL(link) {
+ return errors.New("invalid URL format")
+ }
+
+ parsedURL, err := url.ParseRequestURI(link)
+ if err != nil {
+ return errors.New("invalid URL format")
+ }
+
+ // Check if the scheme is valid
+ if len(scheme) > 0 {
+ if !slices.Contains(scheme, parsedURL.Scheme) {
+ return fmt.Errorf("invalid URL scheme is not part of %v", scheme)
+ }
+ }
+
+ return nil
+}
diff --git a/internal/utils/validation_test.go b/internal/utils/validation_test.go
index f76ec38e2..2561a5e9a 100644
--- a/internal/utils/validation_test.go
+++ b/internal/utils/validation_test.go
@@ -144,7 +144,7 @@ func Test_ValidateDNS(t *testing.T) {
gotError := ValidateDNS(tc.url)
if tc.wantErr != nil {
- assert.EqualErrorf(t, gotError, tc.wantErr.Error(), "ValidateURL(%q) should be '%v', but got '%v'", tc.url, tc.wantErr, gotError)
+ assert.EqualErrorf(t, gotError, tc.wantErr.Error(), "ValidateDNS(%q) should be '%v', but got '%v'", tc.url, tc.wantErr, gotError)
} else {
assert.NoError(t, gotError)
}
@@ -253,3 +253,40 @@ func Test_ValidateNationalIDVerification(t *testing.T) {
})
}
}
+
+func Test_ValidateURLScheme(t *testing.T) {
+ tests := []struct {
+ url string
+ wantErrContains string
+ schemas []string
+ }{
+ {"https://example.com", "", nil},
+ {"https://example.com/page.html", "", nil},
+ {"https://example.com/section", "", nil},
+ {"https://www.example.com", "", nil},
+ {"https://subdomain.example.com", "", nil},
+ {"https://www.subdomain.example.com", "", nil},
+ {"", "invalid URL format", nil},
+ {" ", "invalid URL format", nil},
+ {"foobar", "invalid URL format", nil},
+ {"foobar", "invalid URL format", nil},
+ {"https://", "invalid URL format", nil},
+ {"example.com", "invalid URL format", []string{"https"}},
+ {"ftp://example.com", "invalid URL scheme is not part of [https]", []string{"https"}},
+ {"http://example.com", "invalid URL scheme is not part of [https]", []string{"https"}},
+ {"ftp://example.com", "", []string{"ftp"}},
+ {"http://example.com", "", []string{"http"}},
+ }
+
+ for _, tc := range tests {
+ title := fmt.Sprintf("%s-%s", VisualBool(tc.wantErrContains == ""), tc.url)
+ t.Run(title, func(t *testing.T) {
+ err := ValidateURLScheme(tc.url, tc.schemas...)
+ if tc.wantErrContains == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.ErrorContains(t, err, tc.wantErrContains)
+ }
+ })
+ }
+}
From 74da14334a3f7c837e50a874d9a3b4fcce197dd2 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Tue, 29 Oct 2024 13:19:51 -0700
Subject: [PATCH 50/75] [SDP-1364] Add path validation to the
readDisbursementCSV method used in integration tests (#447)
### What
Add path validation to the readDisbursementCSV method used in integration tests
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1364.
---
internal/integrationtests/utils.go | 10 ++++++++--
internal/integrationtests/utils_test.go | 12 ++++++++++--
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/internal/integrationtests/utils.go b/internal/integrationtests/utils.go
index 4be1232f3..0b5555d6a 100644
--- a/internal/integrationtests/utils.go
+++ b/internal/integrationtests/utils.go
@@ -13,6 +13,7 @@ import (
"github.com/stellar/go/support/log"
"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
// logErrorResponses logs the response body for requests with error status.
@@ -24,16 +25,21 @@ func logErrorResponses(ctx context.Context, body io.ReadCloser) {
}
func readDisbursementCSV(disbursementFilePath string, disbursementFileName string) ([]*data.DisbursementInstruction, error) {
+ err := utils.ValidatePathIsNotTraversal(disbursementFileName)
+ if err != nil {
+ return nil, fmt.Errorf("validating file path: %w", err)
+ }
+
filePath := path.Join(disbursementFilePath, disbursementFileName)
csvBytes, err := fs.ReadFile(DisbursementCSVFiles, filePath)
if err != nil {
- return nil, fmt.Errorf("error reading csv file: %w", err)
+ return nil, fmt.Errorf("reading csv file: %w", err)
}
instructions := []*data.DisbursementInstruction{}
if err = gocsv.UnmarshalBytes(csvBytes, &instructions); err != nil {
- return nil, fmt.Errorf("error parsing csv file: %w", err)
+ return nil, fmt.Errorf("parsing csv file: %w", err)
}
return instructions, nil
diff --git a/internal/integrationtests/utils_test.go b/internal/integrationtests/utils_test.go
index 5d537ca39..784f046ac 100644
--- a/internal/integrationtests/utils_test.go
+++ b/internal/integrationtests/utils_test.go
@@ -36,9 +36,17 @@ func Test_logErrorResponses(t *testing.T) {
}
func Test_readDisbursementCSV(t *testing.T) {
+ t.Run("error if file path is traversal", func(t *testing.T) {
+ expectedError := "validating file path: path cannot contain path traversal"
+
+ data, err := readDisbursementCSV("resources", "../invalid_traversal_path.csv")
+ require.EqualError(t, err, expectedError)
+ assert.Empty(t, data)
+ })
+
t.Run("error trying read csv file", func(t *testing.T) {
filePath := path.Join("resources", "invalid_file.csv")
- expectedError := fmt.Sprintf("error reading csv file: open %s: file does not exist", filePath)
+ expectedError := fmt.Sprintf("reading csv file: open %s: file does not exist", filePath)
data, err := readDisbursementCSV("resources", "invalid_file.csv")
require.EqualError(t, err, expectedError)
@@ -47,7 +55,7 @@ func Test_readDisbursementCSV(t *testing.T) {
t.Run("error opening empty csv file", func(t *testing.T) {
data, err := readDisbursementCSV("resources", "empty_csv_file.csv")
- require.EqualError(t, err, "error parsing csv file: empty csv file given")
+ require.EqualError(t, err, "parsing csv file: empty csv file given")
assert.Empty(t, data)
})
From df15f140ddf81933251459f8b16c4bf1ce90deb5 Mon Sep 17 00:00:00 2001
From: Marwen Abid
Date: Wed, 30 Oct 2024 15:04:31 -0700
Subject: [PATCH 51/75] [SDP-1374] Refactor ReceiverWallet Update function
(#449)
* SDP-1374 Refactor ReceiverWallet Update function
* SDP-1374 address PR comments
---
internal/data/fixtures.go | 28 ++-
internal/data/fixtures_test.go | 6 +-
.../data/receiver_wallets_state_machine.go | 15 ++
.../receiver_wallets_state_machine_test.go | 51 +++-
internal/data/receivers_wallet.go | 147 ++++++++----
internal/data/receivers_wallet_test.go | 220 ++++++++++++------
.../httphandler/payments_handler_test.go | 11 +-
.../httphandler/receiver_handler_test.go | 74 ++----
.../httphandler/receiver_registration_test.go | 7 +-
.../verify_receiver_registration_handler.go | 14 +-
10 files changed, 376 insertions(+), 197 deletions(-)
diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go
index 437ef291b..4576f1357 100644
--- a/internal/data/fixtures.go
+++ b/internal/data/fixtures.go
@@ -424,25 +424,29 @@ func DeleteAllReceiverVerificationFixtures(t *testing.T, ctx context.Context, sq
}
func CreateReceiverWalletFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, receiverID, walletID string, status ReceiversWalletStatus) *ReceiverWallet {
- kp, err := keypair.Random()
- require.NoError(t, err)
- stellarAddress := kp.Address()
+ var stellarAddress, stellarMemo, stellarMemoType, anchorPlatformTransactionID string
- randNumber, err := rand.Int(rand.Reader, big.NewInt(90000))
- require.NoError(t, err)
+ if status != DraftReceiversWalletStatus && status != ReadyReceiversWalletStatus {
+ kp, err := keypair.Random()
+ require.NoError(t, err)
+ stellarAddress = kp.Address()
+
+ randNumber, err := rand.Int(rand.Reader, big.NewInt(90000))
+ require.NoError(t, err)
- stellarMemo := fmt.Sprint(randNumber.Int64() + 10000)
- stellarMemoType := "id"
+ stellarMemo = fmt.Sprint(randNumber.Int64() + 10000)
+ stellarMemoType = "id"
- anchorPlatformTransactionID, err := utils.RandomString(10)
- require.NoError(t, err)
+ anchorPlatformTransactionID, err = utils.RandomString(10)
+ require.NoError(t, err)
+ }
const query = `
WITH inserted_receiver_wallet AS (
INSERT INTO receiver_wallets
- (receiver_id, wallet_id, stellar_address, stellar_memo, stellar_memo_type, status, anchor_platform_transaction_id)
+ (receiver_id, wallet_id, stellar_address, stellar_memo, stellar_memo_type, status, status_history, anchor_platform_transaction_id)
VALUES
- ($1, $2, $3, $4, $5, $6, $7)
+ ($1, $2, $3, $4, $5, $6, ARRAY[create_receiver_wallet_status_history(now(), $6)], $7)
RETURNING
*
)
@@ -458,7 +462,7 @@ func CreateReceiverWalletFixture(t *testing.T, ctx context.Context, sqlExec db.S
`
var receiverWallet ReceiverWallet
- err = sqlExec.QueryRowxContext(ctx, query, receiverID, walletID, stellarAddress, stellarMemo, stellarMemoType, status, anchorPlatformTransactionID).Scan(
+ err := sqlExec.QueryRowxContext(ctx, query, receiverID, walletID, stellarAddress, stellarMemo, stellarMemoType, status, anchorPlatformTransactionID).Scan(
&receiverWallet.ID,
&receiverWallet.StellarAddress,
&receiverWallet.StellarMemo,
diff --git a/internal/data/fixtures_test.go b/internal/data/fixtures_test.go
index 7d26ea3a9..1b41adaeb 100644
--- a/internal/data/fixtures_test.go
+++ b/internal/data/fixtures_test.go
@@ -43,17 +43,17 @@ func Test_CreateReceiverWalletFixture(t *testing.T) {
// Create a random receiver wallet
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "My Wallet", "https://mywallet.test.com/", "mywallet.test.com", "mtwallet://")
receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
- rw := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, DraftReceiversWalletStatus)
+ rw := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus)
// Check receiver wallet
require.Len(t, rw.ID, 36)
require.NotEmpty(t, rw.StellarAddress)
require.NotEmpty(t, rw.StellarMemo)
require.NotEmpty(t, rw.StellarMemoType)
- require.Equal(t, DraftReceiversWalletStatus, rw.Status)
+ require.Equal(t, RegisteredReceiversWalletStatus, rw.Status)
require.Len(t, rw.StatusHistory, 1)
require.NotEmpty(t, rw.StatusHistory[0].Timestamp)
- require.Equal(t, DraftReceiversWalletStatus, rw.StatusHistory[0].Status)
+ require.Equal(t, RegisteredReceiversWalletStatus, rw.StatusHistory[0].Status)
require.NotEmpty(t, rw.CreatedAt)
require.NotEmpty(t, rw.UpdatedAt)
diff --git a/internal/data/receiver_wallets_state_machine.go b/internal/data/receiver_wallets_state_machine.go
index b60696fd2..9e443d71a 100644
--- a/internal/data/receiver_wallets_state_machine.go
+++ b/internal/data/receiver_wallets_state_machine.go
@@ -1,5 +1,10 @@
package data
+import (
+ "fmt"
+ "strings"
+)
+
type ReceiversWalletStatus string
const (
@@ -31,3 +36,13 @@ func ReceiversWalletStateMachineWithInitialState(initialState ReceiversWalletSta
func (status ReceiversWalletStatus) State() State {
return State(status)
}
+
+// Validate validates the receiver wallet status
+func (status ReceiversWalletStatus) Validate() error {
+ switch ReceiversWalletStatus(strings.ToUpper(string(status))) {
+ case DraftReceiversWalletStatus, ReadyReceiversWalletStatus, RegisteredReceiversWalletStatus, FlaggedReceiversWalletStatus:
+ return nil
+ default:
+ return fmt.Errorf("invalid receiver wallet status %q", status)
+ }
+}
diff --git a/internal/data/receiver_wallets_state_machine_test.go b/internal/data/receiver_wallets_state_machine_test.go
index a35684aac..9b6d5cdb1 100644
--- a/internal/data/receiver_wallets_state_machine_test.go
+++ b/internal/data/receiver_wallets_state_machine_test.go
@@ -1,6 +1,10 @@
package data
-import "testing"
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
func Test_ReceiversWalletStatus_TransitionTo(t *testing.T) {
tests := []struct {
@@ -61,3 +65,48 @@ func Test_ReceiversWalletStatus_TransitionTo(t *testing.T) {
})
}
}
+
+func Test_ReceiversWalletStatus_Validate(t *testing.T) {
+ tests := []struct {
+ name string
+ status ReceiversWalletStatus
+ err string
+ }{
+ {
+ "validate Draft receiver wallet status",
+ DraftReceiversWalletStatus,
+ "",
+ },
+ {
+ "validate Ready receiver wallet status",
+ ReadyReceiversWalletStatus,
+ "",
+ },
+ {
+ "validate Registered receiver wallet status",
+ RegisteredReceiversWalletStatus,
+ "",
+ },
+ {
+ "validate Flagged receiver wallet status",
+ FlaggedReceiversWalletStatus,
+ "",
+ },
+ {
+ "invalid receiver wallet status",
+ ReceiversWalletStatus("INVALID"),
+ "invalid receiver wallet status \"INVALID\"",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := tt.status.Validate()
+ if tt.err == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tt.err)
+ }
+ })
+ }
+}
diff --git a/internal/data/receivers_wallet.go b/internal/data/receivers_wallet.go
index 9c8b8aecc..2ca5e6734 100644
--- a/internal/data/receivers_wallet.go
+++ b/internal/data/receivers_wallet.go
@@ -7,13 +7,16 @@ import (
"encoding/json"
"errors"
"fmt"
+ "strings"
"time"
"github.com/lib/pq"
"github.com/stellar/go/network"
+ "github.com/stellar/go/strkey"
"github.com/stellar/go/support/log"
"github.com/stellar/stellar-disbursement-platform-backend/db"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
const OTPExpirationTimeMinutes = 30
@@ -382,48 +385,6 @@ func (rw *ReceiverWalletModel) GetByReceiverIDAndWalletDomain(ctx context.Contex
return &receiverWallet, nil
}
-// UpdateReceiverWallet updates the status, address, OTP confirmation time, and anchor platform transaction ID of a
-// receiver wallet.
-func (rw *ReceiverWalletModel) UpdateReceiverWallet(ctx context.Context, receiverWallet ReceiverWallet, sqlExec db.SQLExecuter) error {
- query := `
- UPDATE
- receiver_wallets rw
- SET
- status = $1,
- anchor_platform_transaction_id = $2,
- stellar_address = $3,
- stellar_memo = $4,
- stellar_memo_type = $5,
- otp_confirmed_at = $6,
- otp_confirmed_with = $7
- WHERE rw.id = $8
- `
-
- result, err := sqlExec.ExecContext(ctx, query,
- receiverWallet.Status,
- sql.NullString{String: receiverWallet.AnchorPlatformTransactionID, Valid: receiverWallet.AnchorPlatformTransactionID != ""},
- receiverWallet.StellarAddress,
- sql.NullString{String: receiverWallet.StellarMemo, Valid: receiverWallet.StellarMemo != ""},
- sql.NullString{String: receiverWallet.StellarMemoType, Valid: receiverWallet.StellarMemoType != ""},
- receiverWallet.OTPConfirmedAt,
- receiverWallet.OTPConfirmedWith,
- receiverWallet.ID)
- if err != nil {
- return fmt.Errorf("updating receiver wallet: %w", err)
- }
-
- numRowsAffected, err := result.RowsAffected()
- if err != nil {
- return fmt.Errorf("getting number of rows affected: %w", err)
- }
-
- if numRowsAffected == 0 {
- return fmt.Errorf("no receiver wallet could be found in UpdateReceiverWallet: %w", ErrRecordNotFound)
- }
-
- return nil
-}
-
// VerifyReceiverWalletOTP validates the receiver wallet OTP.
func (rw *ReceiverWalletModel) VerifyReceiverWalletOTP(ctx context.Context, networkPassphrase string, receiverWallet ReceiverWallet, otp string) error {
if networkPassphrase == network.TestNetworkPassphrase {
@@ -616,3 +577,105 @@ func (rw *ReceiverWalletModel) UpdateInvitationSentAt(ctx context.Context, sqlEx
return receiverWallets, nil
}
+
+type ReceiverWalletUpdate struct {
+ Status ReceiversWalletStatus `db:"status"`
+ AnchorPlatformTransactionID string `db:"anchor_platform_transaction_id"`
+ StellarAddress string `db:"stellar_address"`
+ StellarMemo string `db:"stellar_memo"`
+ StellarMemoType string `db:"stellar_memo_type"`
+ OTPConfirmedAt time.Time `db:"otp_confirmed_at"`
+ OTPConfirmedWith string `db:"otp_confirmed_with"`
+}
+
+func (rwu ReceiverWalletUpdate) Validate() error {
+ if utils.IsEmpty(rwu) {
+ return fmt.Errorf("no values provided to update receiver wallet")
+ }
+
+ if rwu.Status != "" {
+ if err := rwu.Status.Validate(); err != nil {
+ return fmt.Errorf("validating status: %w", err)
+ }
+ }
+
+ if rwu.StellarAddress != "" {
+ if !strkey.IsValidEd25519PublicKey(rwu.StellarAddress) {
+ return fmt.Errorf("invalid stellar address")
+ }
+ }
+
+ if !time.Time.IsZero(rwu.OTPConfirmedAt) && rwu.OTPConfirmedWith == "" {
+ return fmt.Errorf("OTPConfirmedWith is required when OTPConfirmedAt is provided")
+ }
+
+ if rwu.OTPConfirmedWith != "" && time.Time.IsZero(rwu.OTPConfirmedAt) {
+ return fmt.Errorf("OTPConfirmedAt is required when OTPConfirmedWith is provided")
+ }
+
+ return nil
+}
+
+func (rw *ReceiverWalletModel) Update(ctx context.Context, id string, update ReceiverWalletUpdate, sqlExec db.SQLExecuter) error {
+ if err := update.Validate(); err != nil {
+ return fmt.Errorf("validating receiver wallet update: %w", err)
+ }
+
+ fields := []string{}
+ args := []interface{}{}
+
+ if update.Status != "" {
+ fields = append(fields, "status = ?")
+ args = append(args, update.Status)
+ fields = append(fields, "status_history = array_prepend(create_receiver_wallet_status_history(NOW(), ?), status_history)")
+ args = append(args, update.Status)
+ }
+ if update.AnchorPlatformTransactionID != "" {
+ fields = append(fields, "anchor_platform_transaction_id = ?")
+ args = append(args, update.AnchorPlatformTransactionID)
+ }
+ if update.StellarAddress != "" {
+ fields = append(fields, "stellar_address = ?")
+ args = append(args, update.StellarAddress)
+ }
+ if update.StellarMemo != "" {
+ fields = append(fields, "stellar_memo = ?")
+ args = append(args, update.StellarMemo)
+ }
+ if update.StellarMemoType != "" {
+ fields = append(fields, "stellar_memo_type = ?")
+ args = append(args, update.StellarMemoType)
+ }
+ if !time.Time.IsZero(update.OTPConfirmedAt) {
+ fields = append(fields, "otp_confirmed_at = ?")
+ args = append(args, update.OTPConfirmedAt)
+ }
+ if update.OTPConfirmedWith != "" {
+ fields = append(fields, "otp_confirmed_with = ?")
+ args = append(args, update.OTPConfirmedWith)
+ }
+
+ args = append(args, id)
+ query := fmt.Sprintf(`
+ UPDATE receiver_wallets
+ SET %s
+ WHERE id = ?
+ `, strings.Join(fields, ", "))
+
+ query = sqlExec.Rebind(query)
+ result, err := sqlExec.ExecContext(ctx, query, args...)
+ if err != nil {
+ return fmt.Errorf("updating receiver wallet: %w", err)
+ }
+
+ numRowsAffected, err := result.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("getting number of rows affected: %w", err)
+ }
+
+ if numRowsAffected == 0 {
+ return fmt.Errorf("no receiver wallet could be found in UpdateReceiverWallet: %w", ErrRecordNotFound)
+ }
+
+ return nil
+}
diff --git a/internal/data/receivers_wallet_test.go b/internal/data/receivers_wallet_test.go
index 5396caa63..e8ef538be 100644
--- a/internal/data/receivers_wallet_test.go
+++ b/internal/data/receivers_wallet_test.go
@@ -520,73 +520,6 @@ func Test_GetByReceiverIDAndWalletDomain(t *testing.T) {
})
}
-func Test_UpdateReceiverWallet(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()
- receiverWalletModel := ReceiverWalletModel{dbConnectionPool: dbConnectionPool}
-
- t.Run("returns error when receiver wallet does not exist", func(t *testing.T) {
- err := receiverWalletModel.UpdateReceiverWallet(ctx, ReceiverWallet{ID: "invalid_id", Status: DraftReceiversWalletStatus}, dbConnectionPool)
- require.ErrorIs(t, err, ErrRecordNotFound)
- })
-
- receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
- wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
- receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, DraftReceiversWalletStatus)
-
- t.Run("returns error when status is not valid", func(t *testing.T) {
- receiverWallet.Status = "invalid_status"
- err := receiverWalletModel.UpdateReceiverWallet(ctx, *receiverWallet, dbConnectionPool)
- require.Error(t, err, "querying receiver wallet: sql: no rows in result set")
- })
-
- t.Run("successfuly update receiver wallet", func(t *testing.T) {
- receiverWallet.AnchorPlatformTransactionID = "test-anchor-tx-platform-id"
- receiverWallet.StellarAddress = "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444"
- receiverWallet.StellarMemo = "123456"
- receiverWallet.StellarMemoType = "id"
- receiverWallet.Status = RegisteredReceiversWalletStatus
- now := time.Now()
- receiverWallet.OTPConfirmedAt = &now
- receiverWallet.OTPConfirmedWith = "test@stellar.org"
-
- err := receiverWalletModel.UpdateReceiverWallet(ctx, *receiverWallet, dbConnectionPool)
- require.NoError(t, err)
-
- // validate if the receiver wallet has been updated
- query := `
- SELECT
- rw.status,
- rw.anchor_platform_transaction_id,
- rw.stellar_address,
- rw.stellar_memo,
- rw.stellar_memo_type,
- otp_confirmed_at,
- COALESCE(rw.otp_confirmed_with, '') as otp_confirmed_with
- FROM
- receiver_wallets rw
- WHERE
- rw.id = $1
- `
- receiverWalletUpdated := ReceiverWallet{}
- err = dbConnectionPool.GetContext(ctx, &receiverWalletUpdated, query, receiverWallet.ID)
- require.NoError(t, err)
-
- assert.Equal(t, RegisteredReceiversWalletStatus, receiverWalletUpdated.Status)
- assert.Equal(t, "test-anchor-tx-platform-id", receiverWalletUpdated.AnchorPlatformTransactionID)
- assert.Equal(t, "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", receiverWalletUpdated.StellarAddress)
- assert.Equal(t, "123456", receiverWalletUpdated.StellarMemo)
- assert.Equal(t, "id", receiverWalletUpdated.StellarMemoType)
- assert.WithinDuration(t, now, *receiverWalletUpdated.OTPConfirmedAt, 100*time.Millisecond)
- assert.Equal(t, receiverWallet.OTPConfirmedWith, receiverWalletUpdated.OTPConfirmedWith)
- })
-}
-
func Test_ReceiverWallet_UpdateOTPByReceiverContactInfoAndWalletDomain(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
@@ -1301,7 +1234,7 @@ func Test_GetByStellarAccountAndMemo(t *testing.T) {
require.Empty(t, actual)
})
- receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, DraftReceiversWalletStatus)
+ receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus)
results, err := receiverWalletModel.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, receiver.PhoneNumber, wallet.SEP10ClientDomain, "123456")
require.NoError(t, err)
require.Equal(t, 1, results)
@@ -1568,3 +1501,154 @@ func Test_ReceiverWalletModelUpdateInvitationSentAt(t *testing.T) {
assert.True(t, invitationSentAt.Before(*receiverWallets[0].InvitationSentAt))
})
}
+
+func Test_ReceiverWalletUpdate_Validate(t *testing.T) {
+ testCases := []struct {
+ name string
+ update ReceiverWalletUpdate
+ err string
+ }{
+ {
+ name: "empty update",
+ update: ReceiverWalletUpdate{},
+ err: "no values provided to update receiver wallet",
+ },
+ {
+ name: "invalid stellar address",
+ update: ReceiverWalletUpdate{
+ StellarAddress: "invalid",
+ },
+ err: "invalid stellar address",
+ },
+ {
+ name: "invalid status",
+ update: ReceiverWalletUpdate{
+ Status: "invalid",
+ },
+ err: "validating status: invalid receiver wallet status \"invalid\"",
+ },
+ {
+ name: "OTPConfirmedAt set without OTPConfirmedWith",
+ update: ReceiverWalletUpdate{
+ OTPConfirmedAt: time.Now(),
+ },
+ err: "OTPConfirmedWith is required when OTPConfirmedAt is provided",
+ },
+ {
+ name: "OTPConfirmedWith set without OTPConfirmedAt",
+ update: ReceiverWalletUpdate{
+ OTPConfirmedWith: "test@email.com",
+ },
+ err: "OTPConfirmedAt is required when OTPConfirmedWith is provided",
+ },
+ {
+ name: "valid update",
+ update: ReceiverWalletUpdate{
+ Status: RegisteredReceiversWalletStatus,
+ StellarAddress: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444",
+ OTPConfirmedAt: time.Now(),
+ OTPConfirmedWith: "test@email.com",
+ },
+ err: "",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := tc.update.Validate()
+ if tc.err == "" {
+ assert.NoError(t, err)
+ } else {
+ assert.EqualError(t, err, tc.err)
+ }
+ })
+ }
+}
+
+func Test_ReceiverWalletModel_Update(t *testing.T) {
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+
+ dbConnectionPool, outerErr := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, outerErr)
+ defer dbConnectionPool.Close()
+
+ ctx := context.Background()
+ receiverWalletModel := ReceiverWalletModel{dbConnectionPool: dbConnectionPool}
+
+ t.Run("returns error when update is empty", func(t *testing.T) {
+ err := receiverWalletModel.Update(ctx, "some-id", ReceiverWalletUpdate{}, dbConnectionPool)
+ assert.EqualError(t, err, "validating receiver wallet update: no values provided to update receiver wallet")
+ })
+
+ t.Run("returns error when receiver wallet does not exist", func(t *testing.T) {
+ update := ReceiverWalletUpdate{
+ Status: RegisteredReceiversWalletStatus,
+ }
+ err := receiverWalletModel.Update(ctx, "invalid_id", update, dbConnectionPool)
+ require.ErrorIs(t, err, ErrRecordNotFound)
+ })
+
+ t.Run("returns error when receiver wallet status is not valid", func(t *testing.T) {
+ update := ReceiverWalletUpdate{
+ Status: "invalid",
+ }
+ err := receiverWalletModel.Update(ctx, "some id", update, dbConnectionPool)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "validating receiver wallet update: validating status: invalid receiver wallet status \"invalid\"")
+ })
+
+ t.Run("successfully updates receiver wallet", func(t *testing.T) {
+ receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
+ wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
+ receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, DraftReceiversWalletStatus)
+
+ now := time.Now()
+
+ update := ReceiverWalletUpdate{
+ Status: RegisteredReceiversWalletStatus,
+ AnchorPlatformTransactionID: "test-tx-id",
+ StellarAddress: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444",
+ StellarMemo: "123456",
+ StellarMemoType: "id",
+ OTPConfirmedAt: now,
+ OTPConfirmedWith: "test@stellar.org",
+ }
+
+ err := receiverWalletModel.Update(ctx, receiverWallet.ID, update, dbConnectionPool)
+ require.NoError(t, err)
+
+ // Verify the update
+ query := `
+ SELECT
+ rw.status,
+ rw.anchor_platform_transaction_id,
+ rw.stellar_address,
+ rw.stellar_memo,
+ rw.stellar_memo_type,
+ rw.otp_confirmed_at,
+ rw.otp_confirmed_with
+ FROM
+ receiver_wallets rw
+ WHERE
+ rw.id = $1
+ `
+ var updated ReceiverWallet
+ err = dbConnectionPool.GetContext(ctx, &updated, query, receiverWallet.ID)
+ require.NoError(t, err)
+
+ assert.Equal(t, RegisteredReceiversWalletStatus, updated.Status)
+ assert.Equal(t, "test-tx-id", updated.AnchorPlatformTransactionID)
+ assert.Equal(t, "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", updated.StellarAddress)
+ assert.Equal(t, "123456", updated.StellarMemo)
+ assert.Equal(t, "id", updated.StellarMemoType)
+ assert.WithinDuration(t, now.UTC(), updated.OTPConfirmedAt.UTC(), time.Microsecond)
+ assert.Equal(t, "test@stellar.org", updated.OTPConfirmedWith)
+
+ // Verify status history was updated
+ var statusHistory ReceiversWalletStatusHistory
+ err = dbConnectionPool.GetContext(ctx, &statusHistory, "SELECT status_history FROM receiver_wallets WHERE id = $1", receiverWallet.ID)
+ require.NoError(t, err)
+ assert.Equal(t, RegisteredReceiversWalletStatus, statusHistory[0].Status)
+ })
+}
diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go
index db4db08ae..78c79a7d6 100644
--- a/internal/serve/httphandler/payments_handler_test.go
+++ b/internal/serve/httphandler/payments_handler_test.go
@@ -150,23 +150,18 @@ func Test_PaymentsHandlerGet(t *testing.T) {
"name": "wallet1",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
- "invitation_sent_at": null,
- "anchor_platform_transaction_id": %q
+ "invitation_sent_at": null
},
"created_at": %q,
"updated_at": %q,
"external_payment_id": %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,
- receiverWallet.StellarMemoType, receiverWallet.CreatedAt.Format(time.RFC3339Nano), receiverWallet.UpdatedAt.Format(time.RFC3339Nano),
- receiverWallet.AnchorPlatformTransactionID, payment.CreatedAt.Format(time.RFC3339Nano), payment.UpdatedAt.Format(time.RFC3339Nano),
+ asset.ID, receiverWallet.ID, receiver.ID, wallet.ID, receiverWallet.CreatedAt.Format(time.RFC3339Nano), receiverWallet.UpdatedAt.Format(time.RFC3339Nano),
+ payment.CreatedAt.Format(time.RFC3339Nano), payment.UpdatedAt.Format(time.RFC3339Nano),
payment.ExternalPaymentID)
assert.JSONEq(t, wantJson, rr.Body.String())
diff --git a/internal/serve/httphandler/receiver_handler_test.go b/internal/serve/httphandler/receiver_handler_test.go
index b3ececed2..507ef66d5 100644
--- a/internal/serve/httphandler/receiver_handler_test.go
+++ b/internal/serve/httphandler/receiver_handler_test.go
@@ -172,9 +172,6 @@ func Test_ReceiverHandlerGet(t *testing.T) {
"sep_10_client_domain": "www.wallet1.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
@@ -192,15 +189,13 @@ func Test_ReceiverHandlerGet(t *testing.T) {
"asset_issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV",
"received_amount": "50.0000000"
}
- ],
- "anchor_platform_transaction_id": %q
+ ]
}
]
}`, receiver.ID, receiver.ExternalID, receiver.Email, receiver.PhoneNumber, receiver.CreatedAt.Format(time.RFC3339Nano),
receiver.UpdatedAt.Format(time.RFC3339Nano), receiverWallet1.ID, receiverWallet1.Receiver.ID, receiverWallet1.Wallet.ID,
- receiverWallet1.StellarAddress, receiverWallet1.StellarMemo, receiverWallet1.StellarMemoType,
receiverWallet1.CreatedAt.Format(time.RFC3339Nano), receiverWallet1.UpdatedAt.Format(time.RFC3339Nano),
- message1.CreatedAt.Format(time.RFC3339Nano), message2.CreatedAt.Format(time.RFC3339Nano), receiverWallet1.AnchorPlatformTransactionID)
+ message1.CreatedAt.Format(time.RFC3339Nano), message2.CreatedAt.Format(time.RFC3339Nano))
assert.JSONEq(t, wantJson, rr.Body.String())
})
@@ -281,9 +276,6 @@ func Test_ReceiverHandlerGet(t *testing.T) {
"sep_10_client_domain": "www.wallet1.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
@@ -301,8 +293,7 @@ func Test_ReceiverHandlerGet(t *testing.T) {
"asset_issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV",
"received_amount": "50.0000000"
}
- ],
- "anchor_platform_transaction_id": %q
+ ]
},
{
"id": %q,
@@ -342,9 +333,8 @@ func Test_ReceiverHandlerGet(t *testing.T) {
]
}`, receiver.ID, receiver.ExternalID, receiver.Email, receiver.PhoneNumber, receiver.CreatedAt.Format(time.RFC3339Nano),
receiver.UpdatedAt.Format(time.RFC3339Nano), receiverWallet1.ID, receiverWallet1.Receiver.ID,
- receiverWallet1.Wallet.ID, receiverWallet1.StellarAddress, receiverWallet1.StellarMemo, receiverWallet1.StellarMemoType,
- receiverWallet1.CreatedAt.Format(time.RFC3339Nano), receiverWallet1.UpdatedAt.Format(time.RFC3339Nano),
- message1.CreatedAt.Format(time.RFC3339Nano), message2.CreatedAt.Format(time.RFC3339Nano), receiverWallet1.AnchorPlatformTransactionID,
+ receiverWallet1.Wallet.ID, receiverWallet1.CreatedAt.Format(time.RFC3339Nano), receiverWallet1.UpdatedAt.Format(time.RFC3339Nano),
+ message1.CreatedAt.Format(time.RFC3339Nano), message2.CreatedAt.Format(time.RFC3339Nano),
receiverWallet2.ID, receiverWallet2.Receiver.ID, receiverWallet2.Wallet.ID,
receiverWallet2.StellarAddress, receiverWallet2.StellarMemo, receiverWallet2.StellarMemoType,
receiverWallet2.CreatedAt.Format(time.RFC3339Nano), receiverWallet2.UpdatedAt.Format(time.RFC3339Nano),
@@ -686,9 +676,6 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"sep_10_client_domain": "www.wallet.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
@@ -699,8 +686,7 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"payments_received": "0",
"failed_payments": "0",
"canceled_payments": "0",
- "remaining_payments": "0",
- "anchor_platform_transaction_id": %q
+ "remaining_payments": "0"
}
]
},
@@ -839,9 +825,8 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
}`,
receiver4.ID, receiver4.CreatedAt.Format(time.RFC3339Nano), receiver4.UpdatedAt.Format(time.RFC3339Nano),
receiverWallet4.ID, receiverWallet4.Receiver.ID, receiverWallet4.Wallet.ID,
- receiverWallet4.StellarAddress, receiverWallet4.StellarMemo, receiverWallet4.StellarMemoType,
receiverWallet4.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.UpdatedAt.Format(time.RFC3339Nano),
- message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.AnchorPlatformTransactionID,
+ message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano),
receiver3.ID, receiver3.CreatedAt.Format(time.RFC3339Nano), receiver3.UpdatedAt.Format(time.RFC3339Nano),
receiverWallet3.ID, receiverWallet3.Receiver.ID, receiverWallet3.Wallet.ID,
receiverWallet3.StellarAddress, receiverWallet3.StellarMemo, receiverWallet3.StellarMemoType,
@@ -1012,9 +997,6 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"sep_10_client_domain": "www.wallet.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
@@ -1025,17 +1007,15 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"payments_received": "0",
"failed_payments": "0",
"canceled_payments": "0",
- "remaining_payments": "0",
- "anchor_platform_transaction_id": %q
+ "remaining_payments": "0"
}
]
}
]
}`, receiver4.ID, receiver4.CreatedAt.Format(time.RFC3339Nano), receiver4.UpdatedAt.Format(time.RFC3339Nano),
receiverWallet4.ID, receiverWallet4.Receiver.ID, receiverWallet4.Wallet.ID,
- receiverWallet4.StellarAddress, receiverWallet4.StellarMemo, receiverWallet4.StellarMemoType,
receiverWallet4.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.UpdatedAt.Format(time.RFC3339Nano),
- message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.AnchorPlatformTransactionID),
+ message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano)),
},
{
name: "fetch receivers with status draft",
@@ -1075,9 +1055,6 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"sep_10_client_domain": "www.wallet.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
@@ -1088,17 +1065,15 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"payments_received": "0",
"failed_payments": "0",
"canceled_payments": "0",
- "remaining_payments": "0",
- "anchor_platform_transaction_id": %q
+ "remaining_payments": "0"
}
]
}
]
}`, receiver4.ID, receiver4.CreatedAt.Format(time.RFC3339Nano), receiver4.UpdatedAt.Format(time.RFC3339Nano),
receiverWallet4.ID, receiverWallet4.Receiver.ID, receiverWallet4.Wallet.ID,
- receiverWallet4.StellarAddress, receiverWallet4.StellarMemo, receiverWallet4.StellarMemoType,
receiverWallet4.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.UpdatedAt.Format(time.RFC3339Nano),
- message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.AnchorPlatformTransactionID),
+ message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano)),
},
{
name: "fetch receivers created before 2023-01-01",
@@ -1168,9 +1143,6 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"sep_10_client_domain": "www.wallet.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
@@ -1181,17 +1153,15 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
"payments_received": "0",
"failed_payments": "0",
"canceled_payments": "0",
- "remaining_payments": "0",
- "anchor_platform_transaction_id": %q
+ "remaining_payments": "0"
}
]
}
]
}`, receiver4.ID, receiver4.CreatedAt.Format(time.RFC3339Nano), receiver4.UpdatedAt.Format(time.RFC3339Nano),
receiverWallet4.ID, receiverWallet4.Receiver.ID, receiverWallet4.Wallet.ID,
- receiverWallet4.StellarAddress, receiverWallet4.StellarMemo, receiverWallet4.StellarMemoType,
receiverWallet4.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.UpdatedAt.Format(time.RFC3339Nano),
- message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano), receiverWallet4.AnchorPlatformTransactionID),
+ message5.CreatedAt.Format(time.RFC3339Nano), message6.CreatedAt.Format(time.RFC3339Nano)),
},
{
name: "fetch receivers created after 2023-01-01 and before 2023-03-01",
@@ -1538,9 +1508,6 @@ func Test_ReceiverHandler_BuildReceiversResponse(t *testing.T) {
"sep_10_client_domain": "www.wallet.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "READY",
"created_at": %q,
"updated_at": %q,
@@ -1551,8 +1518,7 @@ func Test_ReceiverHandler_BuildReceiversResponse(t *testing.T) {
"payments_received": "0",
"failed_payments": "0",
"canceled_payments": "0",
- "remaining_payments": "0",
- "anchor_platform_transaction_id": %q
+ "remaining_payments": "0"
}
]
},
@@ -1582,9 +1548,6 @@ func Test_ReceiverHandler_BuildReceiversResponse(t *testing.T) {
"sep_10_client_domain": "www.wallet.com",
"enabled": true
},
- "stellar_address": %q,
- "stellar_memo": %q,
- "stellar_memo_type": %q,
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
@@ -1595,21 +1558,18 @@ func Test_ReceiverHandler_BuildReceiversResponse(t *testing.T) {
"payments_received": "0",
"failed_payments": "0",
"canceled_payments": "0",
- "remaining_payments": "0",
- "anchor_platform_transaction_id": %q
+ "remaining_payments": "0"
}
]
}
]`, receiver2.ID, receiver2.CreatedAt.Format(time.RFC3339Nano), receiver2.UpdatedAt.Format(time.RFC3339Nano),
receiverWallet2.ID, receiverWallet2.Receiver.ID, receiverWallet2.Wallet.ID,
- receiverWallet2.StellarAddress, receiverWallet2.StellarMemo, receiverWallet2.StellarMemoType,
receiverWallet2.CreatedAt.Format(time.RFC3339Nano), receiverWallet2.UpdatedAt.Format(time.RFC3339Nano),
- message3.CreatedAt.Format(time.RFC3339Nano), message4.CreatedAt.Format(time.RFC3339Nano), receiverWallet2.AnchorPlatformTransactionID,
+ message3.CreatedAt.Format(time.RFC3339Nano), message4.CreatedAt.Format(time.RFC3339Nano),
receiver1.ID, receiver1.CreatedAt.Format(time.RFC3339Nano), receiver1.UpdatedAt.Format(time.RFC3339Nano),
receiverWallet1.ID, receiverWallet1.Receiver.ID, receiverWallet1.Wallet.ID,
- receiverWallet1.StellarAddress, receiverWallet1.StellarMemo, receiverWallet1.StellarMemoType,
receiverWallet1.CreatedAt.Format(time.RFC3339Nano), receiverWallet1.UpdatedAt.Format(time.RFC3339Nano),
- message1.CreatedAt.Format(time.RFC3339Nano), message2.CreatedAt.Format(time.RFC3339Nano), receiverWallet1.AnchorPlatformTransactionID)
+ message1.CreatedAt.Format(time.RFC3339Nano), message2.CreatedAt.Format(time.RFC3339Nano))
assert.JSONEq(t, wantJson, string(ar))
diff --git a/internal/serve/httphandler/receiver_registration_test.go b/internal/serve/httphandler/receiver_registration_test.go
index 7dc943e6d..4e0d9ff02 100644
--- a/internal/serve/httphandler/receiver_registration_test.go
+++ b/internal/serve/httphandler/receiver_registration_test.go
@@ -112,9 +112,10 @@ func Test_ReceiverRegistrationHandler_ServeHTTP(t *testing.T) {
"mywallet://")
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.DraftReceiversWalletStatus)
- receiverWallet.StellarAddress = "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444"
- receiverWallet.StellarMemo = ""
- err = receiverWalletModel.UpdateReceiverWallet(ctx, *receiverWallet, dbConnectionPool)
+ err = receiverWalletModel.Update(ctx, receiverWallet.ID, data.ReceiverWalletUpdate{
+ StellarAddress: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444",
+ StellarMemo: "",
+ }, dbConnectionPool)
require.NoError(t, err)
t.Run("returns 200 - Ok (And show the Registration Success page) if the token is in the request context and it's valid and the user was already registered ๐", func(t *testing.T) {
diff --git a/internal/serve/httphandler/verify_receiver_registration_handler.go b/internal/serve/httphandler/verify_receiver_registration_handler.go
index 0f39c145f..9d7261d7f 100644
--- a/internal/serve/httphandler/verify_receiver_registration_handler.go
+++ b/internal/serve/httphandler/verify_receiver_registration_handler.go
@@ -229,7 +229,14 @@ func (v VerifyReceiverRegistrationHandler) processReceiverWalletOTP(
if sep24Claims.SEP10StellarMemo() != "" {
rw.StellarMemoType = "id"
}
- err = v.Models.ReceiverWallet.UpdateReceiverWallet(ctx, *rw, dbTx)
+ err = v.Models.ReceiverWallet.Update(ctx, rw.ID, data.ReceiverWalletUpdate{
+ Status: rw.Status,
+ StellarAddress: rw.StellarAddress,
+ StellarMemo: rw.StellarMemo,
+ StellarMemoType: rw.StellarMemoType,
+ OTPConfirmedAt: now,
+ OTPConfirmedWith: rw.OTPConfirmedWith,
+ }, dbTx)
if err != nil {
err = fmt.Errorf("completing receiver wallet registration: %w", err)
return receiverWallet, false, err
@@ -242,8 +249,9 @@ func (v VerifyReceiverRegistrationHandler) processReceiverWalletOTP(
// the receiver wallet with the anchor platform transaction ID.
func (v VerifyReceiverRegistrationHandler) processAnchorPlatformID(ctx context.Context, dbTx db.DBTransaction, sep24Claims anchorplatform.SEP24JWTClaims, receiverWallet data.ReceiverWallet) error {
// STEP 1: update receiver wallet with the anchor platform transaction ID.
- receiverWallet.AnchorPlatformTransactionID = sep24Claims.TransactionID()
- err := v.Models.ReceiverWallet.UpdateReceiverWallet(ctx, receiverWallet, dbTx)
+ err := v.Models.ReceiverWallet.Update(ctx, receiverWallet.ID, data.ReceiverWalletUpdate{
+ AnchorPlatformTransactionID: sep24Claims.TransactionID(),
+ }, dbTx)
if err != nil {
return fmt.Errorf("updating receiver wallet with anchor platform transaction ID: %w", err)
}
From ad9bbf8d76eb6d9e00167f9a763483602bedac6e Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Fri, 1 Nov 2024 10:37:44 -0700
Subject: [PATCH 52/75] [SDP-1368] Create `GET /registration-contact-types`
endpoint (#451)
### What
Create `GET /registration-contact-types` endpoint
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1368.
---
.../httphandler/registration_contact_types.go | 27 ++++++++++++++++
.../registration_contact_types_test.go | 32 +++++++++++++++++++
internal/serve/serve.go | 4 +++
internal/serve/serve_test.go | 2 ++
4 files changed, 65 insertions(+)
create mode 100644 internal/serve/httphandler/registration_contact_types.go
create mode 100644 internal/serve/httphandler/registration_contact_types_test.go
diff --git a/internal/serve/httphandler/registration_contact_types.go b/internal/serve/httphandler/registration_contact_types.go
new file mode 100644
index 000000000..7480fc8de
--- /dev/null
+++ b/internal/serve/httphandler/registration_contact_types.go
@@ -0,0 +1,27 @@
+package httphandler
+
+import (
+ "net/http"
+ "slices"
+
+ "github.com/stellar/go/support/render/httpjson"
+ "golang.org/x/exp/maps"
+
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
+)
+
+type RegistrationContactTypesHandler struct{}
+
+func (c RegistrationContactTypesHandler) Get(w http.ResponseWriter, r *http.Request) {
+ allTypes := data.GetAllReceiverContactTypes()
+ allTypesWithWalletAddress := make(map[string]bool, 2*len(allTypes))
+ for _, t := range allTypes {
+ allTypesWithWalletAddress[string(t)] = true
+ allTypesWithWalletAddress[string(t)+"_AND_WALLET_ADDRESS"] = true
+ }
+
+ sortedKeys := maps.Keys(allTypesWithWalletAddress)
+ slices.Sort(sortedKeys)
+
+ httpjson.Render(w, sortedKeys, httpjson.JSON)
+}
diff --git a/internal/serve/httphandler/registration_contact_types_test.go b/internal/serve/httphandler/registration_contact_types_test.go
new file mode 100644
index 000000000..9137064bc
--- /dev/null
+++ b/internal/serve/httphandler/registration_contact_types_test.go
@@ -0,0 +1,32 @@
+package httphandler
+
+import (
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_RegistrationContactTypesHandler_Get(t *testing.T) {
+ h := RegistrationContactTypesHandler{}
+
+ rr := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/receiver-contact-types", nil)
+ require.NoError(t, err)
+ http.HandlerFunc(h.Get).ServeHTTP(rr, req)
+ resp := rr.Result()
+ respBody, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+ expectedJSON := `[
+ "EMAIL",
+ "EMAIL_AND_WALLET_ADDRESS",
+ "PHONE_NUMBER",
+ "PHONE_NUMBER_AND_WALLET_ADDRESS"
+ ]`
+ assert.JSONEq(t, expectedJSON, string(respBody))
+}
diff --git a/internal/serve/serve.go b/internal/serve/serve.go
index ab905f053..54e4dd54f 100644
--- a/internal/serve/serve.go
+++ b/internal/serve/serve.go
@@ -328,6 +328,10 @@ func handleHTTP(o ServeOptions) *chi.Mux {
r.Get("/", httphandler.CountriesHandler{Models: o.Models}.GetCountries)
})
+ r.
+ With(middleware.AnyRoleMiddleware(authManager, data.GetAllRoles()...)).
+ Get("/registration-contact-types", httphandler.RegistrationContactTypesHandler{}.Get)
+
r.Route("/assets", func(r chi.Router) {
assetsHandler := httphandler.AssetsHandler{
Models: o.Models,
diff --git a/internal/serve/serve_test.go b/internal/serve/serve_test.go
index 1ebcd39cb..e25a48a6b 100644
--- a/internal/serve/serve_test.go
+++ b/internal/serve/serve_test.go
@@ -456,6 +456,8 @@ func Test_handleHTTP_authenticatedEndpoints(t *testing.T) {
{http.MethodGet, "/receivers/verification-types"},
// Countries
{http.MethodGet, "/countries"},
+ // Receiver Contact Types
+ {http.MethodGet, "/registration-contact-types"},
// Assets
{http.MethodGet, "/assets"},
{http.MethodPost, "/assets"},
From 46c89baa7e2c3c76c2f9467a1de1be5169af7c9d Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Fri, 1 Nov 2024 10:56:06 -0700
Subject: [PATCH 53/75] [SDP-1370] Update `POST /disbursements` and `GET
/disbursements` APIs to persist and return the Registration Contact Type
(#452)
### What
Update `POST /disbursements` and `GET /disbursements` APIs to persist and return the Registration Contact Type
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1370
---
.github/workflows/e2e_integration_test.yml | 3 +
cmd/integration_tests.go | 11 +++
cmd/utils/custom_set_value.go | 14 +++
cmd/utils/custom_set_value_test.go | 52 ++++++++++
...07-15.0-migrate-data-v1-to-v2-function.sql | 5 +-
...ursement-add-registration-contact-type.sql | 26 +++++
internal/data/disbursements.go | 9 +-
internal/data/disbursements_test.go | 1 +
internal/data/fixtures.go | 7 ++
internal/data/payments.go | 1 +
internal/data/registration_contact_type.go | 97 +++++++++++++++++++
.../integrationtests/integration_tests.go | 16 +--
.../scripts/e2e_integration_test.sh | 3 +
internal/integrationtests/server_api.go | 2 +-
internal/integrationtests/server_api_test.go | 2 +-
internal/integrationtests/utils.go | 1 +
.../serve/httphandler/disbursement_handler.go | 21 ++--
.../httphandler/disbursement_handler_test.go | 73 ++++++++------
.../serve/httphandler/payments_handler.go | 8 +-
.../httphandler/payments_handler_test.go | 3 +-
.../httphandler/registration_contact_types.go | 14 +--
21 files changed, 298 insertions(+), 71 deletions(-)
create mode 100644 db/migrations/sdp-migrations/2024-10-31.0-disbursement-add-registration-contact-type.sql
create mode 100644 internal/data/registration_contact_type.go
diff --git a/.github/workflows/e2e_integration_test.yml b/.github/workflows/e2e_integration_test.yml
index de3d443ca..9d653b94c 100644
--- a/.github/workflows/e2e_integration_test.yml
+++ b/.github/workflows/e2e_integration_test.yml
@@ -33,14 +33,17 @@ jobs:
environment: "Receiver Registration - E2E Integration Tests (Stellar)"
DISTRIBUTION_ACCOUNT_TYPE: "DISTRIBUTION_ACCOUNT.STELLAR.ENV"
DISBURSEMENT_CSV_FILE_NAME: "disbursement_instructions_phone.csv"
+ REGISTRATION_CONTACT_TYPE: "PHONE_NUMBER"
- platform: "Stellar-email"
environment: "Receiver Registration - E2E Integration Tests (Stellar)"
DISTRIBUTION_ACCOUNT_TYPE: "DISTRIBUTION_ACCOUNT.STELLAR.ENV"
DISBURSEMENT_CSV_FILE_NAME: "disbursement_instructions_email.csv"
+ REGISTRATION_CONTACT_TYPE: "EMAIL"
- platform: "Circle-phone"
environment: "Receiver Registration - E2E Integration Tests (Circle)"
DISTRIBUTION_ACCOUNT_TYPE: "DISTRIBUTION_ACCOUNT.CIRCLE.DB_VAULT"
DISBURSEMENT_CSV_FILE_NAME: "disbursement_instructions_phone.csv"
+ REGISTRATION_CONTACT_TYPE: "PHONE_NUMBER"
environment: ${{ matrix.environment }}
env:
DISTRIBUTION_ACCOUNT_TYPE: ${{ matrix.DISTRIBUTION_ACCOUNT_TYPE }}
diff --git a/cmd/integration_tests.go b/cmd/integration_tests.go
index b4db44045..c9593b3fc 100644
--- a/cmd/integration_tests.go
+++ b/cmd/integration_tests.go
@@ -1,6 +1,7 @@
package cmd
import (
+ "fmt"
"go/types"
"github.com/spf13/cobra"
@@ -8,6 +9,7 @@ import (
"github.com/stellar/go/support/log"
cmdUtils "github.com/stellar/stellar-disbursement-platform-backend/cmd/utils"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/internal/integrationtests"
)
@@ -105,6 +107,15 @@ func (c *IntegrationTestsCommand) Command() *cobra.Command {
ConfigKey: &integrationTestsOpts.ServerApiBaseURL,
Required: true,
},
+ {
+ Name: "registration-contact-type",
+ Usage: fmt.Sprintf("The registration contact type used when creating a new disbursement. Options: %v", data.AllRegistrationContactTypes()),
+ OptType: types.String,
+ CustomSetValue: cmdUtils.SetRegistrationContactType,
+ ConfigKey: &integrationTestsOpts.RegistrationContactType,
+ Required: true,
+ FlagDefault: data.RegistrationContactTypePhone.String(),
+ },
}
integrationTestsCmd := &cobra.Command{
Use: "integration-tests",
diff --git a/cmd/utils/custom_set_value.go b/cmd/utils/custom_set_value.go
index a7ed5753f..da09dfc6a 100644
--- a/cmd/utils/custom_set_value.go
+++ b/cmd/utils/custom_set_value.go
@@ -13,6 +13,7 @@ import (
"github.com/stellar/go/support/log"
"github.com/stellar/stellar-disbursement-platform-backend/internal/crashtracker"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/internal/events"
"github.com/stellar/stellar-disbursement-platform-backend/internal/message"
"github.com/stellar/stellar-disbursement-platform-backend/internal/monitor"
@@ -287,3 +288,16 @@ func SetConfigOptionKafkaSecurityProtocol(co *config.ConfigOption) error {
*(co.ConfigKey.(*events.KafkaSecurityProtocol)) = protocolParsed
return nil
}
+
+func SetRegistrationContactType(co *config.ConfigOption) error {
+ regAccountTypeStr := viper.GetString(co.Name)
+ regAccountType := data.RegistrationContactType{}
+
+ err := regAccountType.ParseFromString(regAccountTypeStr)
+ if err != nil {
+ return fmt.Errorf("couldn't parse registration contact type in %s: %w", co.Name, err)
+ }
+
+ *(co.ConfigKey.(*data.RegistrationContactType)) = regAccountType
+ return nil
+}
diff --git a/cmd/utils/custom_set_value_test.go b/cmd/utils/custom_set_value_test.go
index 4c2bb5bd7..b04ab56e3 100644
--- a/cmd/utils/custom_set_value_test.go
+++ b/cmd/utils/custom_set_value_test.go
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stellar/stellar-disbursement-platform-backend/internal/crashtracker"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/internal/events"
"github.com/stellar/stellar-disbursement-platform-backend/internal/message"
"github.com/stellar/stellar-disbursement-platform-backend/internal/monitor"
@@ -715,3 +716,54 @@ func Test_SetConfigOptionEventBrokerType(t *testing.T) {
})
}
}
+
+func Test_SetRegistrationContactType(t *testing.T) {
+ opts := struct{ RegistrationContactType data.RegistrationContactType }{}
+
+ co := config.ConfigOption{
+ Name: "registration-contact-type",
+ OptType: types.String,
+ CustomSetValue: SetRegistrationContactType,
+ ConfigKey: &opts.RegistrationContactType,
+ }
+
+ testCases := []customSetterTestCase[data.RegistrationContactType]{
+ {
+ name: "returns an error if the value is empty",
+ args: []string{},
+ wantErrContains: `couldn't parse registration contact type in registration-contact-type: unknown ReceiverContactType ""`,
+ },
+ {
+ name: "returns an error if the value is not supported",
+ args: []string{"--registration-contact-type", "test"},
+ wantErrContains: `couldn't parse registration contact type in registration-contact-type: unknown ReceiverContactType "TEST"`,
+ },
+ {
+ name: "๐ handles registration contact type (through CLI args): EMAIL",
+ args: []string{"--registration-contact-type", "EmAiL"},
+ wantResult: data.RegistrationContactTypeEmail,
+ },
+ {
+ name: "๐ handles registration contact type (through CLI args): EMAIL_AND_WALLET_ADDRESS",
+ args: []string{"--registration-contact-type", "EMAIL_AND_WALLET_ADDRESS"},
+ wantResult: data.RegistrationContactTypeEmailAndWalletAddress,
+ },
+ {
+ name: "๐ handles registration contact type (through CLI args): PHONE_NUMBER",
+ args: []string{"--registration-contact-type", "PHONE_NUMBER"},
+ wantResult: data.RegistrationContactTypePhone,
+ },
+ {
+ name: "๐ handles registration contact type (through CLI args): PHONE_NUMBER_AND_WALLET_ADDRESS",
+ args: []string{"--registration-contact-type", "PHONE_NUMBER_AND_WALLET_ADDRESS"},
+ wantResult: data.RegistrationContactTypePhoneAndWalletAddress,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ opts.RegistrationContactType = data.RegistrationContactType{}
+ customSetterTester[data.RegistrationContactType](t, tc, co)
+ })
+ }
+}
diff --git a/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql b/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql
index c60c945ed..00186ad1e 100644
--- a/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql
+++ b/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql
@@ -50,7 +50,8 @@ BEGIN
USING status::text::%I.disbursement_status,
ALTER COLUMN verification_field DROP DEFAULT,
ALTER COLUMN verification_field TYPE %I.verification_type
- USING verification_field::text::%I.verification_type;
+ USING verification_field::text::%I.verification_type,
+ ADD COLUMN IF NOT EXISTS registration_contact_type %I.registration_contact_types NOT NULL DEFAULT ''PHONE_NUMBER'';
INSERT INTO %I.disbursements SELECT * FROM public.disbursements;
ALTER TABLE public.payments
@@ -70,7 +71,7 @@ BEGIN
schema_name, schema_name, schema_name, schema_name, schema_name, schema_name,
schema_name, schema_name, schema_name, schema_name, schema_name, schema_name,
schema_name, schema_name, schema_name, schema_name, schema_name, schema_name,
- schema_name);
+ schema_name, schema_name);
-- Step 3: Import TSS data
diff --git a/db/migrations/sdp-migrations/2024-10-31.0-disbursement-add-registration-contact-type.sql b/db/migrations/sdp-migrations/2024-10-31.0-disbursement-add-registration-contact-type.sql
new file mode 100644
index 000000000..533342650
--- /dev/null
+++ b/db/migrations/sdp-migrations/2024-10-31.0-disbursement-add-registration-contact-type.sql
@@ -0,0 +1,26 @@
+-- This migration adds the receiver_contact_type enum type and the receiver_contact_type column to the disbursements table.
+
+-- +migrate Up
+CREATE TYPE registration_contact_types AS ENUM (
+ 'EMAIL',
+ 'EMAIL_AND_WALLET_ADDRESS',
+ 'PHONE_NUMBER',
+ 'PHONE_NUMBER_AND_WALLET_ADDRESS'
+);
+
+ALTER TABLE disbursements
+ ADD COLUMN registration_contact_type registration_contact_types;
+
+UPDATE disbursements
+ SET registration_contact_type = 'PHONE_NUMBER'
+ WHERE registration_contact_type IS NULL;
+
+ALTER TABLE disbursements
+ ALTER COLUMN registration_contact_type SET NOT NULL;
+
+
+-- +migrate Down
+ALTER TABLE disbursements
+ DROP COLUMN registration_contact_type;
+
+DROP TYPE IF EXISTS registration_contact_types;
diff --git a/internal/data/disbursements.go b/internal/data/disbursements.go
index 188662c66..5bf566fbd 100644
--- a/internal/data/disbursements.go
+++ b/internal/data/disbursements.go
@@ -30,6 +30,7 @@ type Disbursement struct {
FileContent []byte `json:"-" db:"file_content"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
+ RegistrationContactType RegistrationContactType `json:"registration_contact_type,omitempty" db:"registration_contact_type"`
*DisbursementStats
}
@@ -71,9 +72,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, verification_field, receiver_registration_message_template)
+ disbursements (name, status, status_history, wallet_id, asset_id, country_code, verification_field, receiver_registration_message_template, registration_contact_type)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8)
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id
`
var newId string
@@ -86,6 +87,7 @@ func (d *DisbursementModel) Insert(ctx context.Context, disbursement *Disburseme
disbursement.Country.Code,
disbursement.VerificationField,
disbursement.ReceiverRegistrationMessageTemplate,
+ disbursement.RegistrationContactType,
)
if err != nil {
// check if the error is a duplicate key error
@@ -127,6 +129,7 @@ func (d *DisbursementModel) Get(ctx context.Context, sqlExec db.SQLExecuter, id
d.created_at,
d.updated_at,
d.verification_field,
+ d.registration_contact_type,
COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template,
w.id as "wallet.id",
w.name as "wallet.name",
@@ -179,6 +182,7 @@ func (d *DisbursementModel) GetByName(ctx context.Context, sqlExec db.SQLExecute
d.created_at,
d.updated_at,
d.verification_field,
+ d.registration_contact_type,
COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template,
w.id as "wallet.id",
w.name as "wallet.name",
@@ -320,6 +324,7 @@ func (d *DisbursementModel) GetAll(ctx context.Context, sqlExec db.SQLExecuter,
d.created_at,
d.updated_at,
d.verification_field,
+ d.registration_contact_type,
COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template,
COALESCE(d.file_name, '') as file_name,
w.id as "wallet.id",
diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go
index a267c45c7..ed073e661 100644
--- a/internal/data/disbursements_test.go
+++ b/internal/data/disbursements_test.go
@@ -45,6 +45,7 @@ func Test_DisbursementModelInsert(t *testing.T) {
Wallet: wallet,
VerificationField: VerificationTypeDateOfBirth,
ReceiverRegistrationMessageTemplate: smsTemplate,
+ RegistrationContactType: RegistrationContactTypePhone,
}
t.Run("returns error when disbursement already exists", func(t *testing.T) {
diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go
index 4576f1357..6d5b45f75 100644
--- a/internal/data/fixtures.go
+++ b/internal/data/fixtures.go
@@ -576,6 +576,9 @@ func CreateDisbursementFixture(t *testing.T, ctx context.Context, sqlExec db.SQL
if d.VerificationField == "" {
d.VerificationField = VerificationTypeDateOfBirth
}
+ if utils.IsEmpty(d.RegistrationContactType) {
+ d.RegistrationContactType = RegistrationContactTypePhone
+ }
// insert disbursement
if d.StatusHistory == nil {
@@ -650,6 +653,10 @@ func CreateDraftDisbursementFixture(t *testing.T, ctx context.Context, sqlExec d
insert.VerificationField = VerificationTypeDateOfBirth
}
+ if utils.IsEmpty(insert.RegistrationContactType) {
+ insert.RegistrationContactType = RegistrationContactTypePhone
+ }
+
id, err := model.Insert(ctx, &insert)
require.NoError(t, err)
diff --git a/internal/data/payments.go b/internal/data/payments.go
index fbdd34640..490b43b3d 100644
--- a/internal/data/payments.go
+++ b/internal/data/payments.go
@@ -156,6 +156,7 @@ SELECT
d.status as "disbursement.status",
d.created_at as "disbursement.created_at",
d.updated_at as "disbursement.updated_at",
+ d.registration_contact_type as "disbursement.registration_contact_type",
a.id as "asset.id",
a.code as "asset.code",
a.issuer as "asset.issuer",
diff --git a/internal/data/registration_contact_type.go b/internal/data/registration_contact_type.go
new file mode 100644
index 000000000..3bf5dc9f1
--- /dev/null
+++ b/internal/data/registration_contact_type.go
@@ -0,0 +1,97 @@
+package data
+
+import (
+ "database/sql"
+ "database/sql/driver"
+ "encoding/json"
+ "fmt"
+ "slices"
+ "strings"
+)
+
+// RegistrationContactType represents the type of contact information to be used when creating and validating a disbursement.
+type RegistrationContactType struct {
+ ReceiverContactType ReceiverContactType `json:"registration_contact_type"`
+ IncludesWalletAddress bool `json:"includes_wallet_address"`
+}
+
+var (
+ RegistrationContactTypeEmail = RegistrationContactType{ReceiverContactTypeEmail, false}
+ RegistrationContactTypePhone = RegistrationContactType{ReceiverContactTypeSMS, false}
+ RegistrationContactTypeEmailAndWalletAddress = RegistrationContactType{ReceiverContactTypeEmail, true}
+ RegistrationContactTypePhoneAndWalletAddress = RegistrationContactType{ReceiverContactTypeSMS, true}
+)
+
+func (rct RegistrationContactType) String() string {
+ if rct.IncludesWalletAddress {
+ return fmt.Sprintf("%s_AND_WALLET_ADDRESS", rct.ReceiverContactType)
+ }
+ return string(rct.ReceiverContactType)
+}
+
+// ParseFromString parses the string, setting ReceiverContactType and IncludesWalletAddress based on suffix.
+func (rct *RegistrationContactType) ParseFromString(input string) error {
+ input = strings.ToUpper(strings.TrimSpace(input))
+ rct.IncludesWalletAddress = strings.HasSuffix(input, "_AND_WALLET_ADDRESS")
+ rct.ReceiverContactType = ReceiverContactType(strings.TrimSuffix(input, "_AND_WALLET_ADDRESS"))
+
+ if !slices.Contains(GetAllReceiverContactTypes(), rct.ReceiverContactType) {
+ return fmt.Errorf("unknown ReceiverContactType %q", rct.ReceiverContactType)
+ }
+
+ return nil
+}
+
+func AllRegistrationContactTypes() []RegistrationContactType {
+ return []RegistrationContactType{
+ RegistrationContactTypeEmail,
+ RegistrationContactTypeEmailAndWalletAddress,
+ RegistrationContactTypePhone,
+ RegistrationContactTypePhoneAndWalletAddress,
+ }
+}
+
+func (rct RegistrationContactType) MarshalJSON() ([]byte, error) {
+ return json.Marshal(rct.String())
+}
+
+func (rct *RegistrationContactType) UnmarshalJSON(data []byte) error {
+ var strValue string
+ if err := json.Unmarshal(data, &strValue); err != nil {
+ return err
+ }
+
+ if strValue == "" {
+ return nil
+ }
+ return rct.ParseFromString(strValue)
+}
+
+func (rct RegistrationContactType) Value() (driver.Value, error) {
+ return rct.String(), nil
+}
+
+func (rct *RegistrationContactType) Scan(value interface{}) error {
+ if value == nil {
+ return nil
+ }
+
+ byteValue, ok := value.([]byte)
+ if !ok {
+ return fmt.Errorf("unexpected type for RegistrationContactType %T", value)
+ }
+
+ strValue := string(byteValue)
+ if strValue == "" {
+ return nil
+ }
+
+ return rct.ParseFromString(strValue)
+}
+
+var (
+ _ json.Marshaler = (*RegistrationContactType)(nil)
+ _ json.Unmarshaler = (*RegistrationContactType)(nil)
+ _ driver.Valuer = (*RegistrationContactType)(nil)
+ _ sql.Scanner = (*RegistrationContactType)(nil)
+)
diff --git a/internal/integrationtests/integration_tests.go b/internal/integrationtests/integration_tests.go
index de40bbd8e..8c55ac349 100644
--- a/internal/integrationtests/integration_tests.go
+++ b/internal/integrationtests/integration_tests.go
@@ -19,9 +19,7 @@ import (
"github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant"
)
-const (
- paymentProcessTimeSeconds = 30
-)
+const paymentProcessTimeSeconds = 30
type IntegrationTestsInterface interface {
StartIntegrationTests(ctx context.Context, opts IntegrationTestsOpts) error
@@ -33,6 +31,7 @@ type IntegrationTestsOpts struct {
TenantName string
UserEmail string
UserPassword string
+ RegistrationContactType data.RegistrationContactType
DistributionAccountType string
DisbursedAssetCode string
DisbursetAssetIssuer string
@@ -176,11 +175,12 @@ 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,
- VerificationField: data.VerificationTypeDateOfBirth,
+ Name: opts.DisbursementName,
+ CountryCode: "USA",
+ WalletID: wallet.ID,
+ AssetID: asset.ID,
+ VerificationField: data.VerificationTypeDateOfBirth,
+ RegistrationContactType: opts.RegistrationContactType,
})
if err != nil {
return fmt.Errorf("creating disbursement: %w", err)
diff --git a/internal/integrationtests/scripts/e2e_integration_test.sh b/internal/integrationtests/scripts/e2e_integration_test.sh
index 444d7decf..8c6159718 100755
--- a/internal/integrationtests/scripts/e2e_integration_test.sh
+++ b/internal/integrationtests/scripts/e2e_integration_test.sh
@@ -22,6 +22,9 @@ wait_for_server() {
}
accountTypes=("DISTRIBUTION_ACCOUNT.STELLAR.ENV" "DISTRIBUTION_ACCOUNT.CIRCLE.DB_VAULT")
+if [ -z "${REGISTRATION_CONTACT_TYPE+x}" ]; then
+ export REGISTRATION_CONTACT_TYPE="PHONE_NUMBER"
+fi
for accountType in "${accountTypes[@]}"; do
export DISTRIBUTION_ACCOUNT_TYPE=$accountType
if [ $accountType="DISTRIBUTION_ACCOUNT.STELLAR.ENV" ]
diff --git a/internal/integrationtests/server_api.go b/internal/integrationtests/server_api.go
index f36ab72a5..88083db89 100644
--- a/internal/integrationtests/server_api.go
+++ b/internal/integrationtests/server_api.go
@@ -118,7 +118,7 @@ func (sa *ServerApiIntegrationTests) CreateDisbursement(ctx context.Context, aut
if resp.StatusCode/100 != 2 {
logErrorResponses(ctx, resp.Body)
- return nil, fmt.Errorf("error trying to create a new disbursement on the server API")
+ return nil, fmt.Errorf("trying to create a new disbursement on the server API")
}
disbursement := &data.Disbursement{}
diff --git a/internal/integrationtests/server_api_test.go b/internal/integrationtests/server_api_test.go
index e9a83c3f2..a0ddec6ba 100644
--- a/internal/integrationtests/server_api_test.go
+++ b/internal/integrationtests/server_api_test.go
@@ -127,7 +127,7 @@ func Test_CreateDisbursement(t *testing.T) {
httpClientMock.On("Do", mock.AnythingOfType("*http.Request")).Return(response, nil).Once()
d, err := sa.CreateDisbursement(ctx, authToken, reqBody)
- require.EqualError(t, err, "error trying to create a new disbursement on the server API")
+ require.EqualError(t, err, "trying to create a new disbursement on the server API")
assert.Empty(t, d)
httpClientMock.AssertExpectations(t)
diff --git a/internal/integrationtests/utils.go b/internal/integrationtests/utils.go
index 0b5555d6a..e5c748148 100644
--- a/internal/integrationtests/utils.go
+++ b/internal/integrationtests/utils.go
@@ -19,6 +19,7 @@ import (
// logErrorResponses logs the response body for requests with error status.
func logErrorResponses(ctx context.Context, body io.ReadCloser) {
respBody, err := io.ReadAll(body)
+ defer body.Close()
if err == nil {
log.Ctx(ctx).Infof("error message response: %s", string(respBody))
}
diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go
index 4a31ce1eb..1ad2896dc 100644
--- a/internal/serve/httphandler/disbursement_handler.go
+++ b/internal/serve/httphandler/disbursement_handler.go
@@ -42,12 +42,13 @@ 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"`
- VerificationField data.VerificationType `json:"verification_field"`
- ReceiverRegistrationMessageTemplate string `json:"receiver_registration_message_template"`
+ Name string `json:"name"`
+ CountryCode string `json:"country_code"`
+ WalletID string `json:"wallet_id"`
+ AssetID string `json:"asset_id"`
+ VerificationField data.VerificationType `json:"verification_field"`
+ RegistrationContactType data.RegistrationContactType `json:"registration_contact_type"`
+ ReceiverRegistrationMessageTemplate string `json:"receiver_registration_message_template"`
}
type PatchDisbursementStatusRequest struct {
@@ -68,14 +69,17 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req
v.Check(disbursementRequest.CountryCode != "", "country_code", "country_code is required")
v.Check(disbursementRequest.WalletID != "", "wallet_id", "wallet_id is required")
v.Check(disbursementRequest.AssetID != "", "asset_id", "asset_id is required")
-
+ v.Check(
+ slices.Contains(data.AllRegistrationContactTypes(), disbursementRequest.RegistrationContactType),
+ "registration_contact_type",
+ fmt.Sprintf("invalid parameter. valid values are: %v", data.AllRegistrationContactTypes()),
+ )
if v.HasErrors() {
httperror.BadRequest("Request invalid", err, v.Errors).Render(w)
return
}
verificationField := v.ValidateAndGetVerificationType()
-
if v.HasErrors() {
httperror.BadRequest("Verification field invalid", err, v.Errors).Render(w)
return
@@ -127,6 +131,7 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req
Asset: asset,
Country: country,
VerificationField: verificationField,
+ RegistrationContactType: disbursementRequest.RegistrationContactType,
ReceiverRegistrationMessageTemplate: disbursementRequest.ReceiverRegistrationMessageTemplate,
}
diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go
index 4fc86d345..2a3a278c0 100644
--- a/internal/serve/httphandler/disbursement_handler_test.go
+++ b/internal/serve/httphandler/disbursement_handler_test.go
@@ -99,6 +99,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
"wallet_id": "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
"asset_id": "61dbfa89-943a-413c-b862-a2177384d321",
"country_code": "UKR",
+ "registration_contact_type": "PHONE_NUMBER",
"verification_field": "date_of_birth"
}`
@@ -119,6 +120,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
"name": "My New Disbursement name 5",
"asset_id": "61dbfa89-943a-413c-b862-a2177384d321",
"country_code": "UKR",
+ "registration_contact_type": "PHONE_NUMBER",
"verification_field": "date_of_birth"
}`
@@ -133,6 +135,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
"name": "My New Disbursement name 5",
"wallet_id": "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
"country_code": "UKR",
+ "registration_contact_type": "PHONE_NUMBER",
"verification_field": "date_of_birth"
}`
@@ -147,6 +150,7 @@ 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",
+ "registration_contact_type": "PHONE_NUMBER",
"verification_field": "date_of_birth"
}`
@@ -157,10 +161,11 @@ func Test_DisbursementHandler_PostDisbursement(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: enabledWallet.ID,
+ Name: "disbursement 1",
+ CountryCode: country.Code,
+ AssetID: asset.ID,
+ WalletID: enabledWallet.ID,
+ RegistrationContactType: data.RegistrationContactTypePhone,
})
require.NoError(t, err)
@@ -171,11 +176,12 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
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.VerificationTypeDateOfBirth,
+ Name: "disbursement 1",
+ CountryCode: country.Code,
+ AssetID: asset.ID,
+ WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
})
require.NoError(t, err)
@@ -187,11 +193,12 @@ 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,
- VerificationField: data.VerificationTypeDateOfBirth,
+ Name: "disbursement 1",
+ CountryCode: country.Code,
+ AssetID: asset.ID,
+ WalletID: disabledWallet.ID,
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
})
require.NoError(t, err)
@@ -202,11 +209,12 @@ 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,
- VerificationField: data.VerificationTypeDateOfBirth,
+ Name: "disbursement 1",
+ CountryCode: country.Code,
+ AssetID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
+ WalletID: enabledWallet.ID,
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
})
require.NoError(t, err)
@@ -217,11 +225,12 @@ 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,
- VerificationField: data.VerificationTypeDateOfBirth,
+ Name: "disbursement 1",
+ CountryCode: "AAA",
+ AssetID: asset.ID,
+ WalletID: enabledWallet.ID,
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
})
require.NoError(t, err)
@@ -240,19 +249,19 @@ 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,
- VerificationField: data.VerificationTypeDateOfBirth,
+ Name: "disbursement 1",
+ CountryCode: country.Code,
+ AssetID: asset.ID,
+ WalletID: enabledWallet.ID,
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
})
require.NoError(t, err)
- want := `{"error":"disbursement already exists"}`
-
// create disbursement
assertPOSTResponse(t, ctx, handler, method, url, string(requestBody), "", http.StatusCreated)
// try creating again
+ want := `{"error":"disbursement already exists"}`
assertPOSTResponse(t, ctx, handler, method, url, string(requestBody), want, http.StatusConflict)
})
@@ -266,6 +275,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
AssetID: asset.ID,
WalletID: enabledWallet.ID,
VerificationField: data.VerificationTypeDateOfBirth,
+ RegistrationContactType: data.RegistrationContactTypePhone,
ReceiverRegistrationMessageTemplate: smsTemplate,
})
require.NoError(t, err)
@@ -288,6 +298,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
assert.Equal(t, &enabledWallet, actualDisbursement.Wallet)
assert.Equal(t, country, actualDisbursement.Country)
assert.Equal(t, 1, len(actualDisbursement.StatusHistory))
+ assert.Equal(t, data.RegistrationContactTypePhone, actualDisbursement.RegistrationContactType)
assert.Equal(t, data.DraftDisbursementStatus, actualDisbursement.StatusHistory[0].Status)
assert.Equal(t, user.ID, actualDisbursement.StatusHistory[0].UserID)
assert.Equal(t, smsTemplate, actualDisbursement.ReceiverRegistrationMessageTemplate)
diff --git a/internal/serve/httphandler/payments_handler.go b/internal/serve/httphandler/payments_handler.go
index a63be3d4f..d647674e3 100644
--- a/internal/serve/httphandler/payments_handler.go
+++ b/internal/serve/httphandler/payments_handler.go
@@ -85,24 +85,24 @@ func (p PaymentsHandler) decorateWithCircleTransactionInfo(ctx context.Context,
func (p PaymentsHandler) GetPayment(w http.ResponseWriter, r *http.Request) {
paymentID := chi.URLParam(r, "id")
+ ctx := r.Context()
- payment, err := p.Models.Payment.Get(r.Context(), paymentID, p.DBConnectionPool)
+ payment, err := p.Models.Payment.Get(ctx, paymentID, p.DBConnectionPool)
if err != nil {
if errors.Is(err, data.ErrRecordNotFound) {
errorResponse := fmt.Sprintf("Cannot retrieve payment with ID: %s", paymentID)
httperror.NotFound(errorResponse, err, nil).Render(w)
return
} else {
- ctx := r.Context()
msg := fmt.Sprintf("Cannot retrieve payment with id %s", paymentID)
httperror.InternalError(ctx, msg, err, nil).Render(w)
return
}
}
- payments, err := p.decorateWithCircleTransactionInfo(r.Context(), *payment)
+ payments, err := p.decorateWithCircleTransactionInfo(ctx, *payment)
if err != nil {
- httperror.InternalError(r.Context(), "Cannot retrieve payment with circle info", err, nil).Render(w)
+ httperror.InternalError(ctx, "Cannot retrieve payment with circle info", err, nil).Render(w)
return
}
diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go
index 78c79a7d6..fcad3f450 100644
--- a/internal/serve/httphandler/payments_handler_test.go
+++ b/internal/serve/httphandler/payments_handler_test.go
@@ -132,6 +132,7 @@ func Test_PaymentsHandlerGet(t *testing.T) {
"status": "DRAFT",
"created_at": %q,
"updated_at": %q,
+ "registration_contact_type": %q,
"receiver_registration_message_template":""
},
"asset": {
@@ -159,7 +160,7 @@ func Test_PaymentsHandlerGet(t *testing.T) {
"updated_at": %q,
"external_payment_id": %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),
+ disbursement.ID, disbursement.CreatedAt.Format(time.RFC3339Nano), disbursement.UpdatedAt.Format(time.RFC3339Nano), disbursement.RegistrationContactType.String(),
asset.ID, receiverWallet.ID, receiver.ID, wallet.ID, receiverWallet.CreatedAt.Format(time.RFC3339Nano), receiverWallet.UpdatedAt.Format(time.RFC3339Nano),
payment.CreatedAt.Format(time.RFC3339Nano), payment.UpdatedAt.Format(time.RFC3339Nano),
payment.ExternalPaymentID)
diff --git a/internal/serve/httphandler/registration_contact_types.go b/internal/serve/httphandler/registration_contact_types.go
index 7480fc8de..1e75ebb37 100644
--- a/internal/serve/httphandler/registration_contact_types.go
+++ b/internal/serve/httphandler/registration_contact_types.go
@@ -2,10 +2,8 @@ package httphandler
import (
"net/http"
- "slices"
"github.com/stellar/go/support/render/httpjson"
- "golang.org/x/exp/maps"
"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
)
@@ -13,15 +11,5 @@ import (
type RegistrationContactTypesHandler struct{}
func (c RegistrationContactTypesHandler) Get(w http.ResponseWriter, r *http.Request) {
- allTypes := data.GetAllReceiverContactTypes()
- allTypesWithWalletAddress := make(map[string]bool, 2*len(allTypes))
- for _, t := range allTypes {
- allTypesWithWalletAddress[string(t)] = true
- allTypesWithWalletAddress[string(t)+"_AND_WALLET_ADDRESS"] = true
- }
-
- sortedKeys := maps.Keys(allTypesWithWalletAddress)
- slices.Sort(sortedKeys)
-
- httpjson.Render(w, sortedKeys, httpjson.JSON)
+ httpjson.Render(w, data.AllRegistrationContactTypes(), httpjson.JSON)
}
From 54de7c530d60d53e0b3d43569db270441da52865 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Fri, 1 Nov 2024 11:07:20 -0700
Subject: [PATCH 54/75] [SDP-1379] Properly inject the commit hash when running
`make docker-build` (#450)
### What
Properly inject the commit hash when running `make docker-build`.
### Why
Dev instances where not receiving the GIT_COMMIT variable, and it was not being displayed in the /health endpoint.
---
.../anchor_platform_integration_check.yml | 14 +++++++++++++-
.github/workflows/e2e_integration_test.yml | 12 ++++++++++--
...letenant_to_multitenant_db_migration_test.yml | 10 +++++++++-
Dockerfile | 10 ++++++++--
Dockerfile.development | 16 +++++++++++-----
cmd/root.go | 2 --
main.go | 2 ++
7 files changed, 53 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/anchor_platform_integration_check.yml b/.github/workflows/anchor_platform_integration_check.yml
index 765e94651..523c6f9e5 100644
--- a/.github/workflows/anchor_platform_integration_check.yml
+++ b/.github/workflows/anchor_platform_integration_check.yml
@@ -22,9 +22,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ - name: Cleanup data
+ working-directory: dev
+ run: docker compose -f docker-compose-sdp-anchor.yml down -v
+ shell: bash
+
+ - name: Set GIT_COMMIT
+ run: echo "GIT_COMMIT=$(git rev-parse --short HEAD)$( [ -n "$(git status -s)" ] && echo "-dirty-$(id -u -n)" )" >> $GITHUB_ENV
+
+ - name: Build Docker Compose for SDP and Anchor Platform
+ working-directory: dev
+ run: docker compose -f docker-compose-sdp-anchor.yml build --build-arg GIT_COMMIT=${GIT_COMMIT}
+
- name: Run Docker Compose for SDP and Anchor Platform
working-directory: dev
- run: docker compose -f docker-compose-sdp-anchor.yml down && docker compose -f docker-compose-sdp-anchor.yml up --build -d
+ run: docker compose -f docker-compose-sdp-anchor.yml up -d
- name: Install curl
run: sudo apt-get update && sudo apt-get install -y curl
diff --git a/.github/workflows/e2e_integration_test.yml b/.github/workflows/e2e_integration_test.yml
index 9d653b94c..824ef9d71 100644
--- a/.github/workflows/e2e_integration_test.yml
+++ b/.github/workflows/e2e_integration_test.yml
@@ -56,9 +56,17 @@ jobs:
run: docker compose -f docker-compose-e2e-tests.yml down -v
shell: bash
- - name: Run Docker Compose for SDP, Anchor Platform and TSS
+ - name: Set GIT_COMMIT
+ run: echo "GIT_COMMIT=$(git rev-parse --short HEAD)$( [ -n "$(git status -s)" ] && echo "-dirty-$(id -u -n)" )" >> $GITHUB_ENV
+
+ - name: Build Docker Compose services with GIT_COMMIT
+ working-directory: internal/integrationtests/docker
+ run: docker compose -f docker-compose-e2e-tests.yml build --build-arg GIT_COMMIT=${GIT_COMMIT}
+ shell: bash
+
+ - name: Run Docker Compose for SDP, Anchor Platform, and TSS
working-directory: internal/integrationtests/docker
- run: docker compose -f docker-compose-e2e-tests.yml up --build -V -d
+ run: docker compose -f docker-compose-e2e-tests.yml up -d
shell: bash
- name: Install curl
diff --git a/.github/workflows/singletenant_to_multitenant_db_migration_test.yml b/.github/workflows/singletenant_to_multitenant_db_migration_test.yml
index 6b9b0d2e2..23d61e027 100644
--- a/.github/workflows/singletenant_to_multitenant_db_migration_test.yml
+++ b/.github/workflows/singletenant_to_multitenant_db_migration_test.yml
@@ -34,9 +34,17 @@ jobs:
run: docker compose -f docker-compose-e2e-tests.yml down -v
shell: bash
+ - name: Set GIT_COMMIT
+ run: echo "GIT_COMMIT=$(git rev-parse --short HEAD)$( [ -n "$(git status -s)" ] && echo "-dirty-$(id -u -n)" )" >> $GITHUB_ENV
+
+ - name: Build Docker Compose for SDP, Anchor Platform and TSS
+ working-directory: internal/integrationtests/docker
+ run: docker compose -f docker-compose-e2e-tests.yml build --build-arg GIT_COMMIT=${GIT_COMMIT}
+ shell: bash
+
- name: Run Docker Compose for SDP, Anchor Platform and TSS
working-directory: internal/integrationtests/docker
- run: docker compose -f docker-compose-e2e-tests.yml up --build -V -d
+ run: docker compose -f docker-compose-e2e-tests.yml up -d
shell: bash
- name: Install curl
diff --git a/Dockerfile b/Dockerfile
index d6f40831d..372173a7a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,19 +4,25 @@
# make docker-push
FROM golang:1.23.2-bullseye AS build
+
+# Declare the build argument
ARG GIT_COMMIT
WORKDIR /src/stellar-disbursement-platform
ADD go.mod go.sum ./
RUN go mod download
ADD . ./
-RUN go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=$GIT_COMMIT" .
+# Assign GIT_COMMIT in the environment, falling back to the current commit hash if empty
+RUN if [ -z "$GIT_COMMIT" ]; then \
+ GIT_COMMIT=$(git rev-parse --short HEAD)$( [ -n "$(git status -s)" ] && echo "-dirty-$(id -u -n)" ); \
+ fi && \
+ echo "Building with commit: $GIT_COMMIT" && \
+ go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=${GIT_COMMIT}" .
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
-# ADD migrations/ /app/migrations/
COPY --from=build /bin/stellar-disbursement-platform /app/
EXPOSE 8001
WORKDIR /app
diff --git a/Dockerfile.development b/Dockerfile.development
index a4597de90..a35130d46 100644
--- a/Dockerfile.development
+++ b/Dockerfile.development
@@ -1,12 +1,20 @@
# Stage 1: Build the Go application
FROM golang:1.23.2-bullseye AS build
+
+# Declare the build argument
ARG GIT_COMMIT
WORKDIR /src/stellar-disbursement-platform
ADD go.mod go.sum ./
RUN go mod download
COPY . ./
-RUN go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=$GIT_COMMIT" .
+
+# Single RUN command to set GIT_COMMIT and use it in build
+RUN if [ -z "$GIT_COMMIT" ]; then \
+ GIT_COMMIT=$(git rev-parse --short HEAD)$( [ -n "$(git status -s)" ] && echo "-dirty-$(id -u -n)" ); \
+ fi && \
+ echo "Building with commit: $GIT_COMMIT" && \
+ go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=${GIT_COMMIT}" .
# Stage 2: Setup the development environment with Delve for debugging
FROM golang:1.23.2-bullseye AS development
@@ -15,13 +23,11 @@ FROM golang:1.23.2-bullseye AS development
WORKDIR /app/github.com/stellar/stellar-disbursement-platform
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
# Copy the built executable and all source files for debugging
-COPY --from=build /src/stellar-disbursement-platform /app/github.com/stellar/stellar-disbursement-platform
-# Build a debug version of the binary
-RUN go build -gcflags="all=-N -l" -o stellar-disbursement-platform .
+COPY --from=build /bin/stellar-disbursement-platform /app/github.com/stellar/stellar-disbursement-platform/
+COPY . ./
# Install Delve
RUN go install github.com/go-delve/delve/cmd/dlv@latest
# Ensure the binary has executable permissions
RUN chmod +x /app/github.com/stellar/stellar-disbursement-platform/stellar-disbursement-platform
EXPOSE 8001 2345
ENTRYPOINT ["/go/bin/dlv", "exec", "--continue", "--accept-multiclient", "--headless", "--listen=:2345", "--api-version=2", "--log", "./stellar-disbursement-platform"]
-
diff --git a/cmd/root.go b/cmd/root.go
index 39ea9d101..b8b703197 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -81,8 +81,6 @@ func rootCmd() *cobra.Command {
if err != nil {
log.Ctx(ctx).Fatalf("Error setting values of config options: %s", err.Error())
}
- log.Ctx(ctx).Info("Version: ", globalOptions.Version)
- log.Ctx(ctx).Info("GitCommit: ", globalOptions.GitCommit)
},
Run: func(cmd *cobra.Command, args []string) {
err := cmd.Help()
diff --git a/main.go b/main.go
index 3c7ec0d3f..76fbad201 100644
--- a/main.go
+++ b/main.go
@@ -25,6 +25,8 @@ func main() {
}
preConfigureLogger()
+ log.Info("Version: ", Version)
+ log.Info("GitCommit: ", GitCommit)
rootCmd := cmd.SetupCLI(Version, GitCommit)
if err := rootCmd.Execute(); err != nil {
From 9d0d720ec53698804ba256c02701effe5f328c40 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Fri, 1 Nov 2024 12:00:27 -0700
Subject: [PATCH 55/75] [SDP-1370] Refactor to improve code reusability and
testability (#454)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
### What
Refactor to improve code reusability and testability by addressing the subtasks raised in PR #452:
- [x] Add more unit tests to the `POST /disbursements` endpoint to ensure all types of registration contact type are persisted
- [x] Slightly refactor `data/disbursements.go` to make the Selector query string into a common constant
- [x] Update the custom validator of `POST /disburements` so it actually does something. Right now it's barely useful and only increases complexity for no reason.
- [x] Remove the unused file `migrate_1.1.6_to_2.0.0.sh`.
### Why
Raizen ๐
---
dev/migrate_1.1.6_to_2.0.0.sh | 192 -------
.../data/disbursement_instructions_test.go | 27 +-
internal/data/disbursements.go | 94 +---
.../serve/httphandler/disbursement_handler.go | 105 ++--
.../httphandler/disbursement_handler_test.go | 513 +++++++++---------
.../disbursement_request_validator.go | 29 -
.../disbursement_request_validator_test.go | 34 --
7 files changed, 339 insertions(+), 655 deletions(-)
delete mode 100755 dev/migrate_1.1.6_to_2.0.0.sh
delete mode 100644 internal/serve/validators/disbursement_request_validator.go
delete mode 100644 internal/serve/validators/disbursement_request_validator_test.go
diff --git a/dev/migrate_1.1.6_to_2.0.0.sh b/dev/migrate_1.1.6_to_2.0.0.sh
deleted file mode 100755
index add53a313..000000000
--- a/dev/migrate_1.1.6_to_2.0.0.sh
+++ /dev/null
@@ -1,192 +0,0 @@
-#!/bin/bash
-# This script is used to migrate the database from the single-tenant structure to the multi-tenant structure.
-# ATTENTION: make sure to update the variables below before running this script. Refer to the sections marked with ๐.
-set -eu
-
-# Check if curl is installed
-if ! command -v curl &> /dev/null
-then
- echo "Error: curl is not installed. Please install curl to continue."
- exit 1
-fi
-
-# ๐ Remember to update the variables below before running this script.
-singleTenantDBURL="postgres://localhost:5432/sdp-116?sslmode=disable"
-multiTenantDBURL="postgres://localhost:5432/sdp-116-mtn?sslmode=disable"
-multiTenantDBName="sdp-116-mtn"
-psqlDumpOutput="sdp-mainBranch-v1_16.sql"
-
-perform_step() {
- export DIVIDER="--------------------------------------------------------------------------------"
-
- step=$1
- step_name="$2"
- command="$3"
- pause_message="${4:-}"
-
- echo
- echo $DIVIDER
- printf "STEP %s โ: %s\n" "$step" "$step_name"
- eval "$command"
- printf "STEP %s โ
: %s\n" "$step" "$step_name"
- echo $DIVIDER
-
- if [ -n "$pause_message" ]; then
- read -p "$pause_message"
- fi
-}
-
-stepCounter=1
-
-perform_step $((stepCounter++)) "deleting pre-existing single-tenant dump file" "rm -f $psqlDumpOutput" "In the next step, we will delete and recreate the MTN database. Click enter to continue."
-
-perform_step $((stepCounter++)) "deleting and recreating the multi-tenant database" "dropdb $multiTenantDBName && createdb $multiTenantDBName" "In the next step, we will be creating a new dump from the single-tenant DB. Click enter to continue."
-
-perform_step $((stepCounter++)) "dumping single-tenant database" "pg_dump $singleTenantDBURL > $psqlDumpOutput" "In the next step, we will be restoring the single-tenant dump in the multi-tenant database. Click enter to continue."
-
-perform_step $((stepCounter++)) "restoring single-tenant dump into multi-tenant database" "psql -d $multiTenantDBURL < $psqlDumpOutput" "In the next step, we will be running the TSS and Admin migrations. Click enter to continue."
-
-perform_step $((stepCounter++)) "running tss and admin migrations" "go run main.go db admin migrate up && go run main.go db tss migrate up"
-
-
-read -p "๐จ ATTENTION: for the next step, you'll need the admin server to be up and running under http://localhost:8003. Make sure to run 'go run main.go serve', and then hit Enter to continue"
-
-
-# ๐ Remember to update the variables below before running this script.
-tenant="bluecorp"
-create_tenant() {
- ADMIN_ACCOUNT="SDP-admin"
- ADMIN_API_KEY="api_key_1234567890"
- basicAuthCredentials=$(echo -n "$ADMIN_ACCOUNT:$ADMIN_API_KEY" | base64)
- AuthHeader="Authorization: Basic $basicAuthCredentials"
-
- baseURL="http://$tenant.stellar.local:8000"
- sdpUIBaseURL="http://$tenant.stellar.local:3000"
- ownerEmail="owner@$tenant.org"
-
- curl -X POST http://localhost:8003/tenants \
- -H "Content-Type: application/json" \
- -H "$AuthHeader" \
- -d '{
- "name": "'"$tenant"'",
- "organization_name": "Blue Corp",
- "base_url": "'"$baseURL"'",
- "sdp_ui_base_url": "'"$sdpUIBaseURL"'",
- "owner_email": "'"$ownerEmail"'",
- "owner_first_name": "john",
- "owner_last_name": "doe"
- }'
- echo
-}
-
-perform_step $((stepCounter++)) "provisioning tenant $tenant" "create_tenant" "Your tenant was successfully created! Hit enter to copy the single-tenant data to the multi-tenant structure, and dump the single-tenant structure."
-
-sql_script=$(cat < 0 {
diff --git a/internal/serve/validators/disbursement_request_validator.go b/internal/serve/validators/disbursement_request_validator.go
deleted file mode 100644
index ea856a666..000000000
--- a/internal/serve/validators/disbursement_request_validator.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package validators
-
-import (
- "fmt"
- "slices"
-
- "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
-)
-
-type DisbursementRequestValidator struct {
- verificationField data.VerificationType
- *Validator
-}
-
-func NewDisbursementRequestValidator(verificationField data.VerificationType) *DisbursementRequestValidator {
- return &DisbursementRequestValidator{
- verificationField: verificationField,
- Validator: NewValidator(),
- }
-}
-
-// ValidateAndGetVerificationType validates if the verification type field is a valid value.
-func (dv *DisbursementRequestValidator) ValidateAndGetVerificationType() data.VerificationType {
- if !slices.Contains(data.GetAllVerificationTypes(), dv.verificationField) {
- dv.Check(false, "verification_field", fmt.Sprintf("invalid parameter. valid values are: %v", data.GetAllVerificationTypes()))
- return ""
- }
- return dv.verificationField
-}
diff --git a/internal/serve/validators/disbursement_request_validator_test.go b/internal/serve/validators/disbursement_request_validator_test.go
deleted file mode 100644
index 5c5e15399..000000000
--- a/internal/serve/validators/disbursement_request_validator_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package validators
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
-)
-
-func Test_DisbursementRequestValidator_ValidateAndGetVerificationType(t *testing.T) {
- t.Run("Valid verification type", func(t *testing.T) {
- validField := []data.VerificationType{
- data.VerificationTypeDateOfBirth,
- data.VerificationTypeYearMonth,
- data.VerificationTypePin,
- data.VerificationTypeNationalID,
- }
- for _, field := range validField {
- validator := NewDisbursementRequestValidator(field)
- assert.Equal(t, field, validator.ValidateAndGetVerificationType())
- }
- })
-
- t.Run("Invalid verification type", func(t *testing.T) {
- field := data.VerificationType("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 YEAR_MONTH PIN NATIONAL_ID_NUMBER]", validator.Errors["verification_field"])
- })
-}
From e824a7494979521e3e158e27f7fff1b557215c5d Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Fri, 1 Nov 2024 17:01:24 -0700
Subject: [PATCH 56/75] [SDP-1382] Remove countries from the flow and delete
any reference to it from the database (#455)
### What
Remove countries from the flow and delete any reference to it from the database
### Why
It's not needed in the flow anymore.
Addresses https://stellarorg.atlassian.net/browse/SDP-1382.
---
...07-15.0-migrate-data-v1-to-v2-function.sql | 3 +-
.../2023-01-27.1-create-countries-table.sql | 4 +-
...2024-11-01.0-disbursement-drop-country.sql | 236 ++++++++++++++
internal/data/assets_test.go | 6 -
.../data/circle_transfer_requests_test.go | 16 +-
internal/data/countries.go | 69 -----
internal/data/countries_test.go | 66 ----
.../data/disbursement_instructions_test.go | 17 +-
internal/data/disbursement_receivers_test.go | 16 +-
internal/data/disbursements.go | 14 +-
internal/data/disbursements_test.go | 49 +--
internal/data/fixtures.go | 64 ----
internal/data/models.go | 2 -
internal/data/payments_test.go | 111 ++-----
internal/data/receivers_test.go | 23 +-
internal/data/receivers_wallet_test.go | 6 +-
internal/data/roles.go | 2 +-
internal/data/wallets_test.go | 2 +-
.../integrationtests/integration_tests.go | 3 +-
internal/integrationtests/server_api_test.go | 7 +-
internal/integrationtests/validations_test.go | 38 +--
internal/monitor/monitor_labels.go | 10 +-
internal/monitor/prometheus_client_test.go | 7 +-
internal/monitor/prometheus_metrics.go | 2 +-
...h_anchor_platform_transactions_job_test.go | 2 -
...eceiver_wallets_sms_invitation_job_test.go | 291 +++++++++---------
.../serve/httphandler/countries_handler.go | 25 --
.../httphandler/countries_handler_test.go | 54 ----
.../serve/httphandler/disbursement_handler.go | 15 +-
.../httphandler/disbursement_handler_test.go | 83 ++---
.../httphandler/payments_handler_test.go | 71 ++---
.../httphandler/receiver_handler_test.go | 16 +-
.../httphandler/statistics_handler_test.go | 10 +-
...rify_receiver_registration_handler_test.go | 38 +--
internal/serve/serve.go | 4 -
internal/serve/serve_test.go | 2 -
.../circle_reconciliation_service_test.go | 20 +-
.../disbursement_management_service_test.go | 94 +++---
...r_platform_transactions_completion_test.go | 25 --
.../payment_from_submitter_service_test.go | 64 +---
.../payment_management_service_test.go | 10 +-
.../payment_to_submitter_service_test.go | 38 +--
...ready_payments_cancelation_service_test.go | 11 -
...nd_receiver_wallets_invite_service_test.go | 19 +-
.../statistics/calculate_statistics_test.go | 25 +-
.../httphandler/tenants_handler_test.go | 9 +-
.../internal/provisioning/manager_test.go | 2 -
.../services/status_change_validator_test.go | 8 +-
48 files changed, 652 insertions(+), 1057 deletions(-)
create mode 100644 db/migrations/sdp-migrations/2024-11-01.0-disbursement-drop-country.sql
delete mode 100644 internal/data/countries.go
delete mode 100644 internal/data/countries_test.go
delete mode 100644 internal/serve/httphandler/countries_handler.go
delete mode 100644 internal/serve/httphandler/countries_handler_test.go
diff --git a/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql b/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql
index 00186ad1e..520aaaa30 100644
--- a/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql
+++ b/db/migrations/admin-migrations/2024-07-15.0-migrate-data-v1-to-v2-function.sql
@@ -51,7 +51,8 @@ BEGIN
ALTER COLUMN verification_field DROP DEFAULT,
ALTER COLUMN verification_field TYPE %I.verification_type
USING verification_field::text::%I.verification_type,
- ADD COLUMN IF NOT EXISTS registration_contact_type %I.registration_contact_types NOT NULL DEFAULT ''PHONE_NUMBER'';
+ ADD COLUMN IF NOT EXISTS registration_contact_type %I.registration_contact_types NOT NULL DEFAULT ''PHONE_NUMBER'',
+ DROP COLUMN IF EXISTS country_code CASCADE;
INSERT INTO %I.disbursements SELECT * FROM public.disbursements;
ALTER TABLE public.payments
diff --git a/db/migrations/sdp-migrations/2023-01-27.1-create-countries-table.sql b/db/migrations/sdp-migrations/2023-01-27.1-create-countries-table.sql
index df68ba86b..dd1a89d24 100644
--- a/db/migrations/sdp-migrations/2023-01-27.1-create-countries-table.sql
+++ b/db/migrations/sdp-migrations/2023-01-27.1-create-countries-table.sql
@@ -22,8 +22,6 @@ ALTER TABLE disbursements ALTER COLUMN country_code SET NOT NULL;
CREATE TRIGGER refresh_country_updated_at BEFORE UPDATE ON countries FOR EACH ROW EXECUTE PROCEDURE update_at_refresh();
-- +migrate Down
-DROP TRIGGER refresh_country_updated_at ON countries;
-
-ALTER TABLE disbursements DROP COLUMN country_code;
+ALTER TABLE disbursements DROP COLUMN country_code CASCADE;
DROP TABLE countries CASCADE;
diff --git a/db/migrations/sdp-migrations/2024-11-01.0-disbursement-drop-country.sql b/db/migrations/sdp-migrations/2024-11-01.0-disbursement-drop-country.sql
new file mode 100644
index 000000000..9e6bf45e0
--- /dev/null
+++ b/db/migrations/sdp-migrations/2024-11-01.0-disbursement-drop-country.sql
@@ -0,0 +1,236 @@
+-- This migration drops the countries table and any references to it.
+
+-- +migrate Up
+ALTER TABLE disbursements
+ DROP COLUMN country_code;
+
+DROP TABLE countries CASCADE;
+
+
+-- +migrate Down
+CREATE TABLE countries (
+ code VARCHAR(3) PRIMARY KEY,
+ name VARCHAR(100) NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
+ deleted_at TIMESTAMP WITH TIME ZONE,
+ UNIQUE (name),
+ CONSTRAINT country_code_length_check CHECK (char_length(code) = 3)
+);
+
+INSERT INTO countries
+ (code, name)
+VALUES
+ ('AFG', 'Afghanistan'),
+ ('ALB', 'Albania'),
+ ('DZA', 'Algeria'),
+ ('ASM', 'American Samoa'),
+ ('AND', 'Andorra'),
+ ('AGO', 'Angola'),
+ ('ATG', 'Antigua and Barbuda'),
+ ('ARG', 'Argentina'),
+ ('ARM', 'Armenia'),
+ ('ABW', 'Aruba'),
+ ('AUS', 'Australia'),
+ ('AUT', 'Austria'),
+ ('AZE', 'Azerbaijan'),
+ ('BHS', 'Bahamas'),
+ ('BHR', 'Bahrain'),
+ ('BGD', 'Bangladesh'),
+ ('BRB', 'Barbados'),
+ ('BLR', 'Belarus'),
+ ('BEL', 'Belgium'),
+ ('BLZ', 'Belize'),
+ ('BEN', 'Benin'),
+ ('BMU', 'Bermuda'),
+ ('BTN', 'Bhutan'),
+ ('BOL', 'Bolivia'),
+ ('BIH', 'Bosnia and Herzegovina'),
+ ('BWA', 'Botswana'),
+ ('BRA', 'Brazil'),
+ ('BRN', 'Brunei'),
+ ('BGR', 'Bulgaria'),
+ ('BFA', 'Burkina Faso'),
+ ('BDI', 'Burundi'),
+ ('CPV', 'Cabo Verde'),
+ ('KHM', 'Cambodia'),
+ ('CMR', 'Cameroon'),
+ ('CAN', 'Canada'),
+ ('CAF', 'Central African Republic'),
+ ('TCD', 'Chad'),
+ ('CHL', 'Chile'),
+ ('CHN', 'China'),
+ ('COL', 'Colombia'),
+ ('COM', 'Comoros (the)'),
+ ('COG', 'Congo (the)'),
+ ('COK', 'Cook Islands (the)'),
+ ('CRI', 'Costa Rica'),
+ ('HRV', 'Croatia'),
+ ('CYP', 'Cyprus'),
+ ('CZE', 'Czechia'),
+ ('CIV', 'Cรดte d''Ivoire (Ivory Coast)'),
+ ('COD', 'Democratic Republic of the Congo'),
+ ('DNK', 'Denmark'),
+ ('DJI', 'Djibouti'),
+ ('DMA', 'Dominica'),
+ ('DOM', 'Dominican Republic'),
+ ('ECU', 'Ecuador'),
+ ('EGY', 'Egypt'),
+ ('SLV', 'El Salvador'),
+ ('GNQ', 'Equatorial Guinea'),
+ ('ERI', 'Eritrea'),
+ ('EST', 'Estonia'),
+ ('SWZ', 'Eswatini'),
+ ('ETH', 'Ethiopia'),
+ ('FJI', 'Fiji'),
+ ('FIN', 'Finland'),
+ ('FRA', 'France'),
+ ('GUF', 'French Guiana'),
+ ('PYF', 'French Polynesia'),
+ ('ATF', 'French Southern Territories (the)'),
+ ('GAB', 'Gabon'),
+ ('GMB', 'Gambia (the)'),
+ ('GEO', 'Georgia'),
+ ('DEU', 'Germany'),
+ ('GHA', 'Ghana'),
+ ('GRC', 'Greece'),
+ ('GRL', 'Greenland'),
+ ('GRD', 'Grenada'),
+ ('GUM', 'Guam'),
+ ('GTM', 'Guatemala'),
+ ('GIN', 'Guinea'),
+ ('GNB', 'Guinea-Bissau'),
+ ('GUY', 'Guyana'),
+ ('HTI', 'Haiti'),
+ ('HND', 'Honduras'),
+ ('HUN', 'Hungary'),
+ ('ISL', 'Iceland'),
+ ('IND', 'India'),
+ ('IDN', 'Indonesia'),
+ ('IRQ', 'Iraq'),
+ ('IRL', 'Ireland'),
+ ('ISR', 'Israel'),
+ ('ITA', 'Italy'),
+ ('JAM', 'Jamaica'),
+ ('JPN', 'Japan'),
+ ('JOR', 'Jordan'),
+ ('KAZ', 'Kazakhstan'),
+ ('KEN', 'Kenya'),
+ ('KIR', 'Kiribati'),
+ ('KOR', 'South Korea'),
+ ('KWT', 'Kuwait'),
+ ('KGZ', 'Kyrgyzstan'),
+ ('LAO', 'Laos'),
+ ('LVA', 'Latvia'),
+ ('LBN', 'Lebanon'),
+ ('LSO', 'Lesotho'),
+ ('LBR', 'Liberia'),
+ ('LBY', 'Libya'),
+ ('LIE', 'Liechtenstein'),
+ ('LTU', 'Lithuania'),
+ ('LUX', 'Luxembourg'),
+ ('MDG', 'Madagascar'),
+ ('MWI', 'Malawi'),
+ ('MYS', 'Malaysia'),
+ ('MDV', 'Maldives'),
+ ('MLI', 'Mali'),
+ ('MLT', 'Malta'),
+ ('MHL', 'Marshall Islands (the)'),
+ ('MTQ', 'Martinique'),
+ ('MRT', 'Mauritania'),
+ ('MUS', 'Mauritius'),
+ ('MEX', 'Mexico'),
+ ('FSM', 'Micronesia'),
+ ('MDA', 'Moldova'),
+ ('MCO', 'Monaco'),
+ ('MNG', 'Mongolia'),
+ ('MNE', 'Montenegro'),
+ ('MAR', 'Morocco'),
+ ('MOZ', 'Mozambique'),
+ ('MMR', 'Myanmar'),
+ ('NAM', 'Namibia'),
+ ('NRU', 'Nauru'),
+ ('NPL', 'Nepal'),
+ ('NLD', 'Netherlands (the)'),
+ ('NZL', 'New Zealand'),
+ ('NIC', 'Nicaragua'),
+ ('NER', 'Niger'),
+ ('NGA', 'Nigeria'),
+ ('MKD', 'North Macedonia (Republic of)'),
+ ('NOR', 'Norway'),
+ ('OMN', 'Oman'),
+ ('PAK', 'Pakistan'),
+ ('PLW', 'Palau'),
+ ('PAN', 'Panama'),
+ ('PNG', 'Papua New Guinea'),
+ ('PRY', 'Paraguay'),
+ ('PER', 'Peru'),
+ ('PHL', 'Philippines (the)'),
+ ('POL', 'Poland'),
+ ('PRT', 'Portugal'),
+ ('PRI', 'Puerto Rico'),
+ ('QAT', 'Qatar'),
+ ('ROU', 'Romania'),
+ ('RUS', 'Russia'),
+ ('RWA', 'Rwanda'),
+ ('REU', 'Rรฉunion'),
+ ('BLM', 'Saint Barts'),
+ ('KNA', 'Saint Kitts and Nevis'),
+ ('LCA', 'Saint Lucia'),
+ ('MAF', 'Saint Martin'),
+ ('VCT', 'Saint Vincent and the Grenadines'),
+ ('WSM', 'Samoa'),
+ ('SMR', 'San Marino'),
+ ('STP', 'Sao Tome and Principe'),
+ ('SAU', 'Saudi Arabia'),
+ ('SEN', 'Senegal'),
+ ('SRB', 'Serbia'),
+ ('SYC', 'Seychelles'),
+ ('SLE', 'Sierra Leone'),
+ ('SGP', 'Singapore'),
+ ('SVK', 'Slovakia'),
+ ('SVN', 'Slovenia'),
+ ('SLB', 'Solomon Islands'),
+ ('SOM', 'Somalia'),
+ ('ZAF', 'South Africa'),
+ ('SSD', 'South Sudan'),
+ ('ESP', 'Spain'),
+ ('LKA', 'Sri Lanka'),
+ ('SDN', 'Sudan (the)'),
+ ('SUR', 'Suriname'),
+ ('SWE', 'Sweden'),
+ ('CHE', 'Switzerland'),
+ ('TWN', 'Taiwan'),
+ ('TJK', 'Tajikistan'),
+ ('TZA', 'Tanzania'),
+ ('THA', 'Thailand'),
+ ('TLS', 'Timor-Leste'),
+ ('TGO', 'Togo'),
+ ('TON', 'Tonga'),
+ ('TTO', 'Trinidad and Tobago'),
+ ('TUN', 'Tunisia'),
+ ('TUR', 'Turkey'),
+ ('TKM', 'Turkmenistan'),
+ ('TCA', 'Turks and Caicos Islands'),
+ ('TUV', 'Tuvalu'),
+ ('UGA', 'Uganda'),
+ ('UKR', 'Ukraine'),
+ ('ARE', 'United Arab Emirates'),
+ ('GBR', 'United Kingdom'),
+ ('UMI', 'United States Minor Outlying Islands'),
+ ('USA', 'United States of America'),
+ ('URY', 'Uruguay'),
+ ('UZB', 'Uzbekistan'),
+ ('VUT', 'Vanuatu'),
+ ('VEN', 'Venezuela'),
+ ('VNM', 'Vietnam'),
+ ('VGB', 'Virgin Islands (British)'),
+ ('VIR', 'Virgin Islands (U.S.)'),
+ ('YEM', 'Yemen'),
+ ('ZMB', 'Zambia'),
+ ('ZWE', 'Zimbabwe')
+ON CONFLICT DO NOTHING;
+
+ALTER TABLE disbursements
+ ADD COLUMN country_code VARCHAR(3),
+ ADD CONSTRAINT fk_disbursement_country_code FOREIGN KEY (country_code) REFERENCES countries (code);
diff --git a/internal/data/assets_test.go b/internal/data/assets_test.go
index 67e23d8cf..f3588baa0 100644
--- a/internal/data/assets_test.go
+++ b/internal/data/assets_test.go
@@ -451,8 +451,6 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
require.NoError(t, err)
// 1. Create assets, wallets and disbursements:
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "ATL", "Atlantis")
-
asset1 := CreateAssetFixture(t, ctx, dbConnectionPool, "FOO1", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
asset2 := CreateAssetFixture(t, ctx, dbConnectionPool, "FOO2", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -460,28 +458,24 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
walletB := CreateWalletFixture(t, ctx, dbConnectionPool, "walletB", "https://www.b.com", "www.b.com", "b://")
disbursementA1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: walletA,
Status: ReadyDisbursementStatus,
Asset: asset1,
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template A1",
})
disbursementA2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: walletA,
Status: ReadyDisbursementStatus,
Asset: asset2,
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template A2",
})
disbursementB1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: walletB,
Status: ReadyDisbursementStatus,
Asset: asset1,
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template B1",
})
disbursementB2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: walletB,
Status: ReadyDisbursementStatus,
Asset: asset2,
diff --git a/internal/data/circle_transfer_requests_test.go b/internal/data/circle_transfer_requests_test.go
index 5067b6745..1ce965107 100644
--- a/internal/data/circle_transfer_requests_test.go
+++ b/internal/data/circle_transfer_requests_test.go
@@ -346,13 +346,11 @@ func Test_CircleTransferRequestModel_GetOrInsert(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
})
receiverReady := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
rwReady := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverReady.ID, wallet.ID, ReadyReceiversWalletStatus)
@@ -547,13 +545,11 @@ func Test_CircleTransferRequestModel_GetCurrentTransfersForPaymentIDs(t *testing
models, outerErr := NewModels(dbConnectionPool)
require.NoError(t, outerErr)
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
})
receiverReady := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
rwReady := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverReady.ID, wallet.ID, ReadyReceiversWalletStatus)
diff --git a/internal/data/countries.go b/internal/data/countries.go
deleted file mode 100644
index d05eed75a..000000000
--- a/internal/data/countries.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package data
-
-import (
- "context"
- "database/sql"
- "errors"
- "fmt"
- "time"
-
- "github.com/stellar/stellar-disbursement-platform-backend/db"
-)
-
-type Country struct {
- Code string `json:"code" db:"code"`
- Name string `json:"name" db:"name"`
- CreatedAt time.Time `json:"created_at,omitempty" db:"created_at"`
- UpdatedAt time.Time `json:"updated_at,omitempty" db:"updated_at"`
- DeletedAt *time.Time `json:"-" db:"deleted_at"`
-}
-
-type CountryModel struct {
- dbConnectionPool db.DBConnectionPool
-}
-
-func (m *CountryModel) Get(ctx context.Context, code string) (*Country, error) {
- var country Country
- query := `
- SELECT
- c.code,
- c.name,
- c.created_at,
- c.updated_at
- FROM
- countries c
- WHERE
- c.code = $1
- `
-
- err := m.dbConnectionPool.GetContext(ctx, &country, query, code)
- if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
- return nil, ErrRecordNotFound
- }
- return nil, fmt.Errorf("error querying country code %s: %w", code, err)
- }
- return &country, nil
-}
-
-// GetAll returns all countries in the database
-func (m *CountryModel) GetAll(ctx context.Context) ([]Country, error) {
- countries := []Country{}
- query := `
- SELECT
- c.code,
- c.name,
- c.created_at,
- c.updated_at
- FROM
- countries c
- ORDER BY
- c.name ASC
- `
-
- err := m.dbConnectionPool.SelectContext(ctx, &countries, query)
- if err != nil {
- return nil, fmt.Errorf("error querying countries: %w", err)
- }
- return countries, nil
-}
diff --git a/internal/data/countries_test.go b/internal/data/countries_test.go
deleted file mode 100644
index ad3a2ee8f..000000000
--- a/internal/data/countries_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package data
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/stellar/stellar-disbursement-platform-backend/db"
- "github.com/stellar/stellar-disbursement-platform-backend/db/dbtest"
-)
-
-func Test_CountryModelGet(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()
-
- countryModel := &CountryModel{dbConnectionPool: dbConnectionPool}
-
- t.Run("returns error when country is not found", func(t *testing.T) {
- _, err := countryModel.Get(ctx, "not-found")
- require.Error(t, err)
- require.Equal(t, ErrRecordNotFound, err)
- })
-
- t.Run("returns asset successfully", func(t *testing.T) {
- expected := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
- actual, err := countryModel.Get(ctx, "FRA")
- require.NoError(t, err)
-
- assert.Equal(t, expected, actual)
- })
-}
-
-func Test_CountryModelGetAll(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()
-
- countryModel := &CountryModel{dbConnectionPool: dbConnectionPool}
-
- t.Run("returns all countries successfully", func(t *testing.T) {
- expected := ClearAndCreateCountryFixtures(t, ctx, dbConnectionPool)
- actual, err := countryModel.GetAll(ctx)
- require.NoError(t, err)
-
- assert.Equal(t, expected, actual)
- })
-
- t.Run("returns empty array when no countries", func(t *testing.T) {
- DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
- actual, err := countryModel.GetAll(ctx)
- require.NoError(t, err)
-
- assert.Equal(t, []Country{}, actual)
- })
-}
diff --git a/internal/data/disbursement_instructions_test.go b/internal/data/disbursement_instructions_test.go
index 106f96931..317f11ac3 100644
--- a/internal/data/disbursement_instructions_test.go
+++ b/internal/data/disbursement_instructions_test.go
@@ -24,14 +24,12 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
ctx := context.Background()
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, &DisbursementModel{dbConnectionPool: dbConnectionPool}, Disbursement{
- Name: "disbursement1",
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Name: "disbursement1",
+ Asset: asset,
+ Wallet: wallet,
})
di := NewDisbursementInstructionModel(dbConnectionPool)
@@ -259,11 +257,10 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
// New instructions
readyDisbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, &DisbursementModel{dbConnectionPool: dbConnectionPool}, &Disbursement{
- Name: "readyDisbursement",
- Country: country,
- Wallet: wallet,
- Asset: asset,
- Status: ReadyDisbursementStatus,
+ Name: "readyDisbursement",
+ Wallet: wallet,
+ Asset: asset,
+ Status: ReadyDisbursementStatus,
})
newInstruction1 := DisbursementInstruction{
diff --git a/internal/data/disbursement_receivers_test.go b/internal/data/disbursement_receivers_test.go
index 9d964f914..061c199ba 100644
--- a/internal/data/disbursement_receivers_test.go
+++ b/internal/data/disbursement_receivers_test.go
@@ -25,14 +25,12 @@ func Test_DisbursementReceiverModel_Count(t *testing.T) {
paymentModel := &PaymentModel{dbConnectionPool: dbConnectionPool}
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, disbursementModel, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
})
receiver1 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
@@ -87,14 +85,12 @@ func Test_DisbursementReceiverModel_GetAll(t *testing.T) {
paymentModel := &PaymentModel{dbConnectionPool: dbConnectionPool}
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, disbursementModel, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
})
receiver1 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
diff --git a/internal/data/disbursements.go b/internal/data/disbursements.go
index ea55a0e44..e2bef70c5 100644
--- a/internal/data/disbursements.go
+++ b/internal/data/disbursements.go
@@ -19,7 +19,6 @@ import (
type Disbursement struct {
ID string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
- Country *Country `json:"country,omitempty" db:"country"`
Wallet *Wallet `json:"wallet,omitempty" db:"wallet"`
Asset *Asset `json:"asset,omitempty" db:"asset"`
Status DisbursementStatus `json:"status" db:"status"`
@@ -72,9 +71,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, verification_field, receiver_registration_message_template, registration_contact_type)
+ disbursements (name, status, status_history, wallet_id, asset_id, verification_field, receiver_registration_message_template, registration_contact_type)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+ ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
`
var newId string
@@ -84,7 +83,6 @@ func (d *DisbursementModel) Insert(ctx context.Context, disbursement *Disburseme
disbursement.StatusHistory,
disbursement.Wallet.ID,
disbursement.Asset.ID,
- disbursement.Country.Code,
disbursement.VerificationField,
disbursement.ReceiverRegistrationMessageTemplate,
disbursement.RegistrationContactType,
@@ -139,16 +137,11 @@ const selectDisbursementQuery = `
a.code as "asset.code",
a.issuer as "asset.issuer",
a.created_at as "asset.created_at",
- a.updated_at as "asset.updated_at",
- c.code as "country.code",
- c.name as "country.name",
- c.created_at as "country.created_at",
- c.updated_at as "country.updated_at"
+ a.updated_at as "asset.updated_at"
FROM
disbursements d
JOIN wallets w on d.wallet_id = w.id
JOIN assets a on d.asset_id = a.id
- JOIN countries c on d.country_code = c.code
`
func (d *DisbursementModel) Get(ctx context.Context, sqlExec db.SQLExecuter, id string) (*Disbursement, error) {
@@ -259,7 +252,6 @@ func (d *DisbursementModel) Count(ctx context.Context, sqlExec db.SQLExecuter, q
disbursements d
JOIN wallets w on d.wallet_id = w.id
JOIN assets a on d.asset_id = a.id
- JOIN countries c on d.country_code = c.code
`
query, params := d.newDisbursementQuery(baseQuery, queryParams, false)
diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go
index ed073e661..efd51dd84 100644
--- a/internal/data/disbursements_test.go
+++ b/internal/data/disbursements_test.go
@@ -25,7 +25,6 @@ func Test_DisbursementModelInsert(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
wallet.Assets = nil
@@ -41,7 +40,6 @@ func Test_DisbursementModelInsert(t *testing.T) {
},
},
Asset: asset,
- Country: country,
Wallet: wallet,
VerificationField: VerificationTypeDateOfBirth,
ReceiverRegistrationMessageTemplate: smsTemplate,
@@ -68,7 +66,6 @@ func Test_DisbursementModelInsert(t *testing.T) {
assert.Equal(t, "disbursement2", actual.Name)
assert.Equal(t, DraftDisbursementStatus, actual.Status)
assert.Equal(t, asset, actual.Asset)
- assert.Equal(t, country, actual.Country)
assert.Equal(t, wallet, actual.Wallet)
assert.Equal(t, smsTemplate, actual.ReceiverRegistrationMessageTemplate)
assert.Equal(t, 1, len(actual.StatusHistory))
@@ -91,7 +88,6 @@ func Test_DisbursementModelCount(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := Disbursement{
@@ -102,9 +98,8 @@ func Test_DisbursementModelCount(t *testing.T) {
UserID: "user1",
},
},
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Asset: asset,
+ Wallet: wallet,
}
t.Run("returns 0 when no disbursements exist", func(t *testing.T) {
@@ -156,7 +151,6 @@ func Test_DisbursementModelGet(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := Disbursement{
@@ -168,9 +162,8 @@ func Test_DisbursementModelGet(t *testing.T) {
UserID: "user1",
},
},
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Asset: asset,
+ Wallet: wallet,
}
t.Run("returns error when disbursement does not exist", func(t *testing.T) {
@@ -201,7 +194,6 @@ func Test_DisbursementModelGetByName(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := Disbursement{
@@ -213,9 +205,8 @@ func Test_DisbursementModelGetByName(t *testing.T) {
UserID: "user1",
},
},
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Asset: asset,
+ Wallet: wallet,
}
t.Run("returns error when disbursement does not exist", func(t *testing.T) {
@@ -236,7 +227,6 @@ func Test_DisbursementModelGetByName(t *testing.T) {
func Test_DisbursementModelGetAll(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -247,7 +237,6 @@ func Test_DisbursementModelGetAll(t *testing.T) {
paymentModel := PaymentModel{dbConnectionPool: dbConnectionPool}
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := Disbursement{
@@ -258,9 +247,8 @@ func Test_DisbursementModelGetAll(t *testing.T) {
UserID: "user1",
},
},
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Asset: asset,
+ Wallet: wallet,
}
t.Run("returns empty list when no disbursements exist", func(t *testing.T) {
@@ -281,8 +269,8 @@ func Test_DisbursementModelGetAll(t *testing.T) {
actualDisbursements, err := disbursementModel.GetAll(ctx, dbConnectionPool, &QueryParams{})
require.NoError(t, err)
- assert.Equal(t, 2, len(actualDisbursements))
- assert.Equal(t, []*Disbursement{expected1, expected2}, actualDisbursements)
+ assert.Len(t, actualDisbursements, 2)
+ assert.Equal(t, []*Disbursement{expected2, expected1}, actualDisbursements)
})
t.Run("returns disbursements successfully with limit", func(t *testing.T) {
@@ -350,7 +338,7 @@ func Test_DisbursementModelGetAll(t *testing.T) {
assert.Equal(t, []*Disbursement{expected1}, actualDisbursements)
})
- t.Run("returns disbursements successfully with statuses parameter ", func(t *testing.T) {
+ t.Run("returns disbursements successfully with statuses parameter", func(t *testing.T) {
DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
disbursement.Name = "disbursement1"
@@ -372,6 +360,7 @@ func Test_DisbursementModelGetAll(t *testing.T) {
assert.Equal(t, 2, len(actualDisbursements))
assert.Equal(t, []*Disbursement{expected2, expected1}, actualDisbursements)
})
+
t.Run("returns disbursements successfully with stats", func(t *testing.T) {
DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
@@ -508,7 +497,6 @@ func Test_DisbursementModel_Update(t *testing.T) {
func Test_DisbursementModel_CompleteDisbursements(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, outerErr := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, outerErr)
defer dbConnectionPool.Close()
@@ -518,15 +506,6 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
- DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
- DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
- DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
- DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
-
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -539,7 +518,6 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) {
Status: ReadyDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
VerificationField: VerificationTypeDateOfBirth,
})
@@ -567,7 +545,6 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) {
Status: StartedDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
VerificationField: VerificationTypeDateOfBirth,
})
@@ -605,7 +582,6 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) {
Status: StartedDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
VerificationField: VerificationTypeDateOfBirth,
})
@@ -624,7 +600,6 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) {
Status: StartedDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
VerificationField: VerificationTypeDateOfBirth,
})
diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go
index 6d5b45f75..3654b77bb 100644
--- a/internal/data/fixtures.go
+++ b/internal/data/fixtures.go
@@ -226,66 +226,6 @@ func EnableOrDisableWalletFixtures(t *testing.T, ctx context.Context, sqlExec db
require.NoError(t, err)
}
-func GetCountryFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, code string) *Country {
- const query = `
- SELECT
- *
- FROM
- countries
- WHERE
- code = $1
- `
-
- country := &Country{}
- err := sqlExec.GetContext(ctx, country, query, code)
- require.NoError(t, err)
-
- return country
-}
-
-func CreateCountryFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, code, name string) *Country {
- const query = `
- WITH create_country AS (
- INSERT INTO countries
- (code, name)
- VALUES
- ($1, $2)
- ON CONFLICT DO NOTHING
- RETURNING *
- )
- SELECT created_at, updated_at FROM create_country
- UNION ALL
- SELECT created_at, updated_at FROM countries WHERE code = $1 AND name = $2
- `
-
- country := &Country{
- Code: code,
- Name: name,
- }
-
- err := sqlExec.QueryRowxContext(ctx, query, code, name).Scan(&country.CreatedAt, &country.UpdatedAt)
- require.NoError(t, err)
-
- return country
-}
-
-// DeleteAllCountryFixtures deletes all countries in the database
-func DeleteAllCountryFixtures(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter) {
- const query = "DELETE FROM countries"
- _, err := sqlExec.ExecContext(ctx, query)
- require.NoError(t, err)
-}
-
-// ClearAndCreateCountryFixtures deletes all countries in the database then creates new countries for testing
-func ClearAndCreateCountryFixtures(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter) []Country {
- DeleteAllCountryFixtures(t, ctx, sqlExec)
- expected := []Country{
- *CreateCountryFixture(t, ctx, sqlExec, "BRA", "Brazil"),
- *CreateCountryFixture(t, ctx, sqlExec, "UKR", "Ukraine"),
- }
- return expected
-}
-
func CreateReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, r *Receiver) *Receiver {
t.Helper()
@@ -570,9 +510,6 @@ func CreateDisbursementFixture(t *testing.T, ctx context.Context, sqlExec db.SQL
if d.Asset == nil {
d.Asset = GetAssetFixture(t, ctx, sqlExec, FixtureAssetUSDC)
}
- if d.Country == nil {
- d.Country = GetCountryFixture(t, ctx, sqlExec, FixtureCountryUKR)
- }
if d.VerificationField == "" {
d.VerificationField = VerificationTypeDateOfBirth
}
@@ -825,5 +762,4 @@ func DeleteAllFixtures(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter
DeleteAllDisbursementFixtures(t, ctx, sqlExec)
DeleteAllWalletFixtures(t, ctx, sqlExec)
DeleteAllAssetFixtures(t, ctx, sqlExec)
- DeleteAllCountryFixtures(t, ctx, sqlExec)
}
diff --git a/internal/data/models.go b/internal/data/models.go
index 939d41c08..96d0c7961 100644
--- a/internal/data/models.go
+++ b/internal/data/models.go
@@ -16,7 +16,6 @@ var (
type Models struct {
Disbursements *DisbursementModel
Wallets *WalletModel
- Countries *CountryModel
Assets *AssetModel
Organizations *OrganizationModel
Payment *PaymentModel
@@ -37,7 +36,6 @@ func NewModels(dbConnectionPool db.DBConnectionPool) (*Models, error) {
return &Models{
Disbursements: &DisbursementModel{dbConnectionPool: dbConnectionPool},
Wallets: &WalletModel{dbConnectionPool: dbConnectionPool},
- Countries: &CountryModel{dbConnectionPool: dbConnectionPool},
Assets: &AssetModel{dbConnectionPool: dbConnectionPool},
Organizations: &OrganizationModel{dbConnectionPool: dbConnectionPool},
Payment: &PaymentModel{dbConnectionPool: dbConnectionPool},
diff --git a/internal/data/payments_test.go b/internal/data/payments_test.go
index 193bbce89..d7ff29dd4 100644
--- a/internal/data/payments_test.go
+++ b/internal/data/payments_test.go
@@ -26,7 +26,6 @@ func Test_PaymentsModelGet(t *testing.T) {
ctx := context.Background()
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet1 := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
@@ -38,7 +37,6 @@ func Test_PaymentsModelGet(t *testing.T) {
Status: DraftDisbursementStatus,
Asset: asset,
Wallet: wallet1,
- Country: country,
CreatedAt: time.Date(2022, 3, 21, 23, 40, 20, 1431, time.UTC),
})
@@ -84,11 +82,10 @@ func Test_PaymentsModelGet(t *testing.T) {
receiverWallet2 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet2.ID, DraftReceiversWalletStatus)
disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, &disbursementModel, &Disbursement{
- Name: "disbursement 2",
- Status: DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet2,
- Country: country,
+ Name: "disbursement 2",
+ Status: DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet2,
})
stellarTransactionID, err := utils.RandomString(64)
@@ -130,7 +127,6 @@ func Test_PaymentModelCount(t *testing.T) {
ctx := context.Background()
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
@@ -138,19 +134,17 @@ func Test_PaymentModelCount(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, &disbursementModel, &Disbursement{
- Name: "disbursement 1",
- Status: DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, &disbursementModel, &Disbursement{
- Name: "disbursement 2",
- Status: DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 2",
+ Status: DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
paymentModel := PaymentModel{dbConnectionPool: dbConnectionPool}
@@ -232,7 +226,6 @@ func Test_PaymentModelGetAll(t *testing.T) {
ctx := context.Background()
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
@@ -240,19 +233,17 @@ func Test_PaymentModelGetAll(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, &disbursementModel, &Disbursement{
- Name: "disbursement 1",
- Status: DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, &disbursementModel, &Disbursement{
- Name: "disbursement 2",
- Status: DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 2",
+ Status: DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
paymentModel := PaymentModel{dbConnectionPool: dbConnectionPool}
@@ -393,14 +384,12 @@ func Test_PaymentModelGetAll(t *testing.T) {
DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
usdc := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
demoWallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Demo Wallet", "https://demo-wallet.stellar.org", "https://demo-wallet.stellar.org", "demo-wallet-server.stellar.org")
vibrantWallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Vibrant Assist", "https://vibrantapp.com", "api-dev.vibrantapp.com", "https://vibrantapp.com/sdp-dev")
@@ -409,19 +398,17 @@ func Test_PaymentModelGetAll(t *testing.T) {
receiverVibrantWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, vibrantWallet.ID, ReadyReceiversWalletStatus)
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Name: "disbursement 1",
- Status: ReadyDisbursementStatus,
- Asset: usdc,
- Wallet: demoWallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: ReadyDisbursementStatus,
+ Asset: usdc,
+ Wallet: demoWallet,
})
disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Name: "disbursement 2",
- Status: ReadyDisbursementStatus,
- Asset: usdc,
- Wallet: vibrantWallet,
- Country: country,
+ Name: "disbursement 2",
+ Status: ReadyDisbursementStatus,
+ Asset: usdc,
+ Wallet: vibrantWallet,
})
demoWalletPayment := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
@@ -715,7 +702,6 @@ func Test_PaymentNewPaymentQuery(t *testing.T) {
func Test_PaymentModelRetryFailedPayments(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -725,15 +711,6 @@ func Test_PaymentModelRetryFailedPayments(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
- DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
- DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
- DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
- DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
-
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -741,7 +718,6 @@ func Test_PaymentModelRetryFailedPayments(t *testing.T) {
receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, ReadyReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: ReadyDisbursementStatus,
@@ -998,13 +974,11 @@ func Test_PaymentModelCancelPayment(t *testing.T) {
DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1012,7 +986,6 @@ func Test_PaymentModelCancelPayment(t *testing.T) {
receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, ReadyReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: ReadyDisbursementStatus,
@@ -1260,7 +1233,6 @@ func Test_PaymentModel_GetReadyByDisbursementID(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1271,7 +1243,6 @@ func Test_PaymentModel_GetReadyByDisbursementID(t *testing.T) {
rw2 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet.ID, ReadyReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1326,7 +1297,6 @@ func Test_PaymentModel_GetReadyByPaymentsID(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1337,7 +1307,6 @@ func Test_PaymentModel_GetReadyByPaymentsID(t *testing.T) {
rw2 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet.ID, ReadyReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1400,7 +1369,6 @@ func Test_PaymentModel_GetReadyByReceiverWalletID(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1411,7 +1379,6 @@ func Test_PaymentModel_GetReadyByReceiverWalletID(t *testing.T) {
rw2 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet.ID, ReadyReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1487,7 +1454,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
t.Run("doesn't get payments when receiver wallet is not registered", func(t *testing.T) {
DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1495,7 +1461,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, ReadyReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1522,7 +1487,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
t.Run("doesn't get payments not in the Success or Failed statuses", func(t *testing.T) {
DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1530,7 +1494,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1555,7 +1518,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
t.Run("gets only payments in the Success or Failed statuses", func(t *testing.T) {
DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1565,7 +1527,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
receiverWallet2 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet.ID, RegisteredReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1608,7 +1569,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
t.Run("gets more than one payment when a receiver has payments in the Success or Failed statuses for the same wallet provider", func(t *testing.T) {
DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1616,7 +1576,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus)
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1624,7 +1583,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
})
disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1667,7 +1625,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
t.Run("gets more than one payment when a receiver has payments for more than one wallet provider", func(t *testing.T) {
DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet1 := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet1", "https://www.wallet1.com", "www.wallet1.com", "wallet1://")
wallet2 := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet2", "https://www.wallet2.com", "www.wallet2.com", "wallet2://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1677,7 +1634,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
receiverWallet2 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet2.ID, RegisteredReceiversWalletStatus)
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet1,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1685,7 +1641,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
})
disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet2,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1742,7 +1697,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
t.Run("doesn't return error when receiver wallet has the stellar_memo and stellar_memo_type null", func(t *testing.T) {
DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet1", "https://www.wallet1.com", "www.wallet1.com", "wallet1://")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -1750,7 +1704,6 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing
receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus)
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: StartedDisbursementStatus,
@@ -1889,13 +1842,11 @@ func Test_PaymentModel_UpdateStatus(t *testing.T) {
models, err := NewModels(dbConnectionPool)
require.NoError(t, err)
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
})
receiverReady := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
rwReady := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverReady.ID, wallet.ID, ReadyReceiversWalletStatus)
diff --git a/internal/data/receivers_test.go b/internal/data/receivers_test.go
index 098ad3ec2..eec8c4d21 100644
--- a/internal/data/receivers_test.go
+++ b/internal/data/receivers_test.go
@@ -28,7 +28,6 @@ func Test_ReceiversModel_Get(t *testing.T) {
ctx := context.Background()
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
@@ -36,10 +35,9 @@ func Test_ReceiversModel_Get(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
disbursement := Disbursement{
- Status: DraftDisbursementStatus,
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Status: DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
}
stellarTransactionID, err := utils.RandomString(64)
@@ -929,7 +927,6 @@ func Test_DeleteByContactInfo(t *testing.T) {
})
// 1. Create country, asset, and wallet (won't be deleted)
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "ATL", "Atlantis")
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "FOO1", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "walletA", "https://www.a.com", "www.a.com", "a://")
@@ -951,10 +948,9 @@ func Test_DeleteByContactInfo(t *testing.T) {
CreatedAt: time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC),
})
disbursement1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
})
paymentX1 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
ReceiverWallet: receiverWalletX,
@@ -982,10 +978,9 @@ func Test_DeleteByContactInfo(t *testing.T) {
CreatedAt: time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC),
})
disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
- Country: country,
- Wallet: wallet,
- Status: ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: ReadyDisbursementStatus,
+ Asset: asset,
})
paymentY2 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{
ReceiverWallet: receiverWalletY,
diff --git a/internal/data/receivers_wallet_test.go b/internal/data/receivers_wallet_test.go
index e8ef538be..f2a670009 100644
--- a/internal/data/receivers_wallet_test.go
+++ b/internal/data/receivers_wallet_test.go
@@ -65,7 +65,6 @@ func Test_ReceiversWalletModelGetWithReceiverId(t *testing.T) {
})
asset := CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet1 := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiverWallet1 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet1.ID, DraftReceiversWalletStatus)
@@ -92,9 +91,8 @@ func Test_ReceiversWalletModelGetWithReceiverId(t *testing.T) {
disbursementModel := DisbursementModel{dbConnectionPool: dbConnectionPool}
disbursement := Disbursement{
- Status: DraftDisbursementStatus,
- Asset: asset,
- Country: country,
+ Status: DraftDisbursementStatus,
+ Asset: asset,
}
stellarTransactionID, err := utils.RandomString(64)
diff --git a/internal/data/roles.go b/internal/data/roles.go
index b24414690..d8f03f736 100644
--- a/internal/data/roles.go
+++ b/internal/data/roles.go
@@ -20,7 +20,7 @@ const (
OwnerUserRole UserRole = "owner"
// FinancialControllerUserRole has the same permissions as the OwnerUserRole except for user management.
FinancialControllerUserRole UserRole = "financial_controller"
- // DeveloperUserRole has only configuration permissions. (wallets, assets, countries management. Also, statistics access permission)
+ // DeveloperUserRole has only configuration permissions. (wallets, assets management. Also, statistics access permission)
DeveloperUserRole UserRole = "developer"
// BusinessUserRole has read-only permissions - except for user management that they can't read any data.
BusinessUserRole UserRole = "business"
diff --git a/internal/data/wallets_test.go b/internal/data/wallets_test.go
index 7ffc34360..225e3011c 100644
--- a/internal/data/wallets_test.go
+++ b/internal/data/wallets_test.go
@@ -251,7 +251,7 @@ func Test_WalletModelInsert(t *testing.T) {
})
// Ensure that only insert one of each entry
- t.Run("duplicated countries codes and assets IDs", func(t *testing.T) {
+ t.Run("duplicated assets IDs", func(t *testing.T) {
DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
name := "test_wallet"
diff --git a/internal/integrationtests/integration_tests.go b/internal/integrationtests/integration_tests.go
index 8c55ac349..b687c665e 100644
--- a/internal/integrationtests/integration_tests.go
+++ b/internal/integrationtests/integration_tests.go
@@ -115,7 +115,7 @@ func NewIntegrationTestsService(opts IntegrationTestsOpts) (*IntegrationTestsSer
return it, nil
}
-func (it *IntegrationTestsService) initServices(ctx context.Context, opts IntegrationTestsOpts) {
+func (it *IntegrationTestsService) initServices(_ context.Context, opts IntegrationTestsOpts) {
// initialize default testnet horizon client
it.horizonClient = horizonclient.DefaultTestNetClient
@@ -176,7 +176,6 @@ 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,
VerificationField: data.VerificationTypeDateOfBirth,
diff --git a/internal/integrationtests/server_api_test.go b/internal/integrationtests/server_api_test.go
index a0ddec6ba..495878680 100644
--- a/internal/integrationtests/server_api_test.go
+++ b/internal/integrationtests/server_api_test.go
@@ -103,10 +103,9 @@ func Test_CreateDisbursement(t *testing.T) {
}
reqBody := &httphandler.PostDisbursementRequest{
- Name: "mockDisbursement",
- CountryCode: "USA",
- WalletID: "123",
- AssetID: "890",
+ Name: "mockDisbursement",
+ WalletID: "123",
+ AssetID: "890",
}
t.Run("error calling httpClient.Do", func(t *testing.T) {
diff --git a/internal/integrationtests/validations_test.go b/internal/integrationtests/validations_test.go
index c94e76f2a..864700ba9 100644
--- a/internal/integrationtests/validations_test.go
+++ b/internal/integrationtests/validations_test.go
@@ -30,16 +30,14 @@ func Test_validationAfterProcessDisbursement(t *testing.T) {
})
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
t.Run("invalid disbursement status", func(t *testing.T) {
invalidDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "Invalid Disbursement",
- Status: data.CompletedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "Invalid Disbursement",
+ Status: data.CompletedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
err = validateExpectationsAfterProcessDisbursement(ctx, invalidDisbursement.ID, models, dbConnectionPool)
@@ -47,11 +45,10 @@ func Test_validationAfterProcessDisbursement(t *testing.T) {
})
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
t.Run("disbursement receivers not found", func(t *testing.T) {
@@ -135,16 +132,14 @@ func Test_validationAfterStartDisbursement(t *testing.T) {
})
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
t.Run("invalid disbursement status", func(t *testing.T) {
invalidDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "Invalid Disbursement",
- Status: data.CompletedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "Invalid Disbursement",
+ Status: data.CompletedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
err = validateExpectationsAfterStartDisbursement(ctx, invalidDisbursement.ID, models, dbConnectionPool)
@@ -152,11 +147,10 @@ func Test_validationAfterStartDisbursement(t *testing.T) {
})
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
t.Run("disbursement receivers not found", func(t *testing.T) {
diff --git a/internal/monitor/monitor_labels.go b/internal/monitor/monitor_labels.go
index f34cb775e..f220549a1 100644
--- a/internal/monitor/monitor_labels.go
+++ b/internal/monitor/monitor_labels.go
@@ -11,16 +11,14 @@ type DBQueryLabels struct {
}
type DisbursementLabels struct {
- Asset string
- Country string
- Wallet string
+ Asset string
+ Wallet string
}
func (d DisbursementLabels) ToMap() map[string]string {
return map[string]string{
- "asset": d.Asset,
- "country": d.Country,
- "wallet": d.Wallet,
+ "asset": d.Asset,
+ "wallet": d.Wallet,
}
}
diff --git a/internal/monitor/prometheus_client_test.go b/internal/monitor/prometheus_client_test.go
index c475fcec2..0a78c2935 100644
--- a/internal/monitor/prometheus_client_test.go
+++ b/internal/monitor/prometheus_client_test.go
@@ -166,9 +166,8 @@ func Test_PrometheusClient_MonitorCounters(t *testing.T) {
t.Run("disbursements counter metric", func(t *testing.T) {
labels := DisbursementLabels{
- Asset: "USDC",
- Country: "UKR",
- Wallet: "Mock Wallet",
+ Asset: "USDC",
+ Wallet: "Mock Wallet",
}
mPrometheusClient.MonitorCounters(DisbursementsCounterTag, labels.ToMap())
@@ -185,7 +184,7 @@ func Test_PrometheusClient_MonitorCounters(t *testing.T) {
assert.NotEmpty(t, data)
body := string(data)
- metric := `sdp_business_disbursements_counter{asset="USDC",country="UKR",wallet="Mock Wallet"} 1`
+ metric := `sdp_business_disbursements_counter{asset="USDC",wallet="Mock Wallet"} 1`
assert.Contains(t, body, metric)
diff --git a/internal/monitor/prometheus_metrics.go b/internal/monitor/prometheus_metrics.go
index 91cac7220..2f7eab541 100644
--- a/internal/monitor/prometheus_metrics.go
+++ b/internal/monitor/prometheus_metrics.go
@@ -72,7 +72,7 @@ var CounterVecMetrics = map[MetricTag]*prometheus.CounterVec{
Namespace: "sdp", Subsystem: "business", Name: string(DisbursementsCounterTag),
Help: "Disbursements Counter",
},
- []string{"asset", "country", "wallet"},
+ []string{"asset", "wallet"},
),
CircleAPIRequestsTotalTag: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "sdp", Subsystem: "circle", Name: string(CircleAPIRequestsTotalTag),
diff --git a/internal/scheduler/jobs/patch_anchor_platform_transactions_job_test.go b/internal/scheduler/jobs/patch_anchor_platform_transactions_job_test.go
index b0366454a..bc6667c2e 100644
--- a/internal/scheduler/jobs/patch_anchor_platform_transactions_job_test.go
+++ b/internal/scheduler/jobs/patch_anchor_platform_transactions_job_test.go
@@ -137,7 +137,6 @@ func Test_PatchAnchorPlatformTransactionsCompletionJob_Execute(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -145,7 +144,6 @@ func Test_PatchAnchorPlatformTransactionsCompletionJob_Execute(t *testing.T) {
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
diff --git a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go
index b0637880c..2500212d8 100644
--- a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go
+++ b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job_test.go
@@ -119,7 +119,6 @@ func Test_SendReceiverWalletsSMSInvitationJob(t *testing.T) {
func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -138,126 +137,113 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {
stellarSecretKey := "SBUSPEKAZKLZSWHRSJ2HWDZUK6I3IVDUWA7JJZSGBLZ2WZIUJI7FPNB5"
var maxInvitationSMSResendAttempts int64 = 3
- t.Run("executes the service successfully", func(t *testing.T) {
- messageDispatcherMock := message.NewMockMessageDispatcher(t)
- crashTrackerClientMock := &crashtracker.MockCrashTrackerClient{}
-
- s, err := services.NewSendReceiverWalletInviteService(
- models,
- messageDispatcherMock,
- stellarSecretKey,
- maxInvitationSMSResendAttempts,
- crashTrackerClientMock,
- )
- require.NoError(t, err)
-
- data.DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- data.ClearAndCreateWalletFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllMessagesFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
-
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "ATL", "Atlantis")
-
- wallet1 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet1", "https://wallet1.com", "www.wallet1.com", "wallet1://sdp")
- wallet2 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet2", "https://wallet2.com", "www.wallet2.com", "wallet2://sdp")
-
- asset1 := data.CreateAssetFixture(t, ctx, dbConnectionPool, "FOO1", "GCKGCKZ2PFSCRQXREJMTHAHDMOZQLS2R4V5LZ6VLU53HONH5FI6ACBSX")
- asset2 := data.CreateAssetFixture(t, ctx, dbConnectionPool, "FOO2", "GCKGCKZ2PFSCRQXREJMTHAHDMOZQLS2R4V5LZ6VLU53HONH5FI6ACBSX")
-
- receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
- receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
-
- disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet1,
- Status: data.ReadyDisbursementStatus,
- Asset: asset1,
- })
-
- disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet2,
- Status: data.ReadyDisbursementStatus,
- Asset: asset2,
- })
-
- rec1RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.ReadyReceiversWalletStatus)
- data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet2.ID, data.RegisteredReceiversWalletStatus)
-
- rec2RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet2.ID, data.ReadyReceiversWalletStatus)
-
- _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
- Status: data.ReadyPaymentStatus,
- Disbursement: disbursement1,
- Asset: *asset1,
- ReceiverWallet: rec1RW,
- Amount: "1",
- })
-
- _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
- Status: data.ReadyPaymentStatus,
- Disbursement: disbursement2,
- Asset: *asset2,
- ReceiverWallet: rec2RW,
- Amount: "1",
- })
-
- walletDeepLink1 := services.WalletDeepLink{
- DeepLink: wallet1.DeepLinkSchema,
- TenantBaseURL: tenantBaseURL,
- OrganizationName: "MyCustomAid",
- AssetCode: asset1.Code,
- AssetIssuer: asset1.Issuer,
- }
- deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey)
- require.NoError(t, err)
- contentWallet1 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink1)
- titleWallet1 := "You have a payment waiting for you from " + walletDeepLink1.OrganizationName
-
- walletDeepLink2 := services.WalletDeepLink{
- DeepLink: wallet2.DeepLinkSchema,
- TenantBaseURL: tenantBaseURL,
- OrganizationName: "MyCustomAid",
- AssetCode: asset2.Code,
- AssetIssuer: asset2.Issuer,
- }
- deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey)
- require.NoError(t, err)
- contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2)
- titleWallet2 := "You have a payment waiting for you from " + walletDeepLink2.OrganizationName
-
- mockErr := errors.New("unexpected error")
- messageDispatcherMock.
- On("SendMessage", mock.Anything, message.Message{
- ToPhoneNumber: receiver1.PhoneNumber,
- ToEmail: receiver1.Email,
- Body: contentWallet1,
- Title: titleWallet1,
- }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
- Return(message.MessengerTypeTwilioSMS, mockErr).
- Once().
- On("SendMessage", mock.Anything, message.Message{
- ToPhoneNumber: receiver2.PhoneNumber,
- ToEmail: receiver2.Email,
- Body: contentWallet2,
- Title: titleWallet2,
- }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
- Return(message.MessengerTypeTwilioSMS, nil).
- Once()
-
- mockMsg := fmt.Sprintf(
- "error sending message to receiver ID %s for receiver wallet ID %s using messenger type %s",
- receiver1.ID, rec1RW.ID, message.MessengerTypeTwilioSMS,
- )
- crashTrackerClientMock.On("LogAndReportErrors", ctx, mockErr, mockMsg).Once()
-
- err = s.SendInvite(ctx)
- require.NoError(t, err)
-
- q := `
+ messageDispatcherMock := message.NewMockMessageDispatcher(t)
+ crashTrackerClientMock := &crashtracker.MockCrashTrackerClient{}
+
+ s, err := services.NewSendReceiverWalletInviteService(
+ models,
+ messageDispatcherMock,
+ stellarSecretKey,
+ maxInvitationSMSResendAttempts,
+ crashTrackerClientMock,
+ )
+ require.NoError(t, err)
+
+ wallet1 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet1", "https://wallet1.com", "www.wallet1.com", "wallet1://sdp")
+ wallet2 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet2", "https://wallet2.com", "www.wallet2.com", "wallet2://sdp")
+
+ asset1 := data.CreateAssetFixture(t, ctx, dbConnectionPool, "FOO1", "GCKGCKZ2PFSCRQXREJMTHAHDMOZQLS2R4V5LZ6VLU53HONH5FI6ACBSX")
+ asset2 := data.CreateAssetFixture(t, ctx, dbConnectionPool, "FOO2", "GCKGCKZ2PFSCRQXREJMTHAHDMOZQLS2R4V5LZ6VLU53HONH5FI6ACBSX")
+
+ receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
+ receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
+
+ disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
+ Wallet: wallet1,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset1,
+ })
+
+ disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
+ Wallet: wallet2,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset2,
+ })
+
+ rec1RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.ReadyReceiversWalletStatus)
+ data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet2.ID, data.RegisteredReceiversWalletStatus)
+
+ rec2RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet2.ID, data.ReadyReceiversWalletStatus)
+
+ _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
+ Status: data.ReadyPaymentStatus,
+ Disbursement: disbursement1,
+ Asset: *asset1,
+ ReceiverWallet: rec1RW,
+ Amount: "1",
+ })
+
+ _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
+ Status: data.ReadyPaymentStatus,
+ Disbursement: disbursement2,
+ Asset: *asset2,
+ ReceiverWallet: rec2RW,
+ Amount: "1",
+ })
+
+ walletDeepLink1 := services.WalletDeepLink{
+ DeepLink: wallet1.DeepLinkSchema,
+ TenantBaseURL: tenantBaseURL,
+ OrganizationName: "MyCustomAid",
+ AssetCode: asset1.Code,
+ AssetIssuer: asset1.Issuer,
+ }
+ deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey)
+ require.NoError(t, err)
+ contentWallet1 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink1)
+ titleWallet1 := "You have a payment waiting for you from " + walletDeepLink1.OrganizationName
+
+ walletDeepLink2 := services.WalletDeepLink{
+ DeepLink: wallet2.DeepLinkSchema,
+ TenantBaseURL: tenantBaseURL,
+ OrganizationName: "MyCustomAid",
+ AssetCode: asset2.Code,
+ AssetIssuer: asset2.Issuer,
+ }
+ deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey)
+ require.NoError(t, err)
+ contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2)
+ titleWallet2 := "You have a payment waiting for you from " + walletDeepLink2.OrganizationName
+
+ mockErr := errors.New("unexpected error")
+ messageDispatcherMock.
+ On("SendMessage", mock.Anything, message.Message{
+ ToPhoneNumber: receiver1.PhoneNumber,
+ ToEmail: receiver1.Email,
+ Body: contentWallet1,
+ Title: titleWallet1,
+ }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
+ Return(message.MessengerTypeTwilioSMS, mockErr).
+ Once().
+ On("SendMessage", mock.Anything, message.Message{
+ ToPhoneNumber: receiver2.PhoneNumber,
+ ToEmail: receiver2.Email,
+ Body: contentWallet2,
+ Title: titleWallet2,
+ }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
+ Return(message.MessengerTypeTwilioSMS, nil).
+ Once()
+
+ mockMsg := fmt.Sprintf(
+ "error sending message to receiver ID %s for receiver wallet ID %s using messenger type %s",
+ receiver1.ID, rec1RW.ID, message.MessengerTypeTwilioSMS,
+ )
+ crashTrackerClientMock.On("LogAndReportErrors", ctx, mockErr, mockMsg).Once()
+
+ err = s.SendInvite(ctx)
+ require.NoError(t, err)
+
+ q := `
SELECT
type, status, receiver_id, wallet_id, receiver_wallet_id,
title_encrypted, text_encrypted, status_history
@@ -266,36 +252,35 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {
WHERE
receiver_id = $1 AND wallet_id = $2 AND receiver_wallet_id = $3
`
- var msg data.Message
- err = dbConnectionPool.GetContext(ctx, &msg, q, receiver1.ID, wallet1.ID, rec1RW.ID)
- require.NoError(t, err)
-
- assert.Equal(t, message.MessengerTypeTwilioSMS, msg.Type)
- assert.Equal(t, receiver1.ID, msg.ReceiverID)
- assert.Equal(t, wallet1.ID, msg.WalletID)
- assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID)
- assert.Equal(t, data.FailureMessageStatus, msg.Status)
- assert.Equal(t, titleWallet1, msg.TitleEncrypted)
- assert.Equal(t, contentWallet1, msg.TextEncrypted)
- assert.Len(t, msg.StatusHistory, 2)
- assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status)
- assert.Equal(t, data.FailureMessageStatus, msg.StatusHistory[1].Status)
- assert.Nil(t, msg.AssetID)
-
- msg = data.Message{}
- err = dbConnectionPool.GetContext(ctx, &msg, q, receiver2.ID, wallet2.ID, rec2RW.ID)
- require.NoError(t, err)
-
- assert.Equal(t, message.MessengerTypeTwilioSMS, msg.Type)
- assert.Equal(t, receiver2.ID, msg.ReceiverID)
- assert.Equal(t, wallet2.ID, msg.WalletID)
- assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID)
- assert.Equal(t, data.SuccessMessageStatus, msg.Status)
- assert.Equal(t, titleWallet2, msg.TitleEncrypted)
- assert.Equal(t, contentWallet2, msg.TextEncrypted)
- assert.Len(t, msg.StatusHistory, 2)
- assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status)
- assert.Equal(t, data.SuccessMessageStatus, msg.StatusHistory[1].Status)
- assert.Nil(t, msg.AssetID)
- })
+ var msg data.Message
+ err = dbConnectionPool.GetContext(ctx, &msg, q, receiver1.ID, wallet1.ID, rec1RW.ID)
+ require.NoError(t, err)
+
+ assert.Equal(t, message.MessengerTypeTwilioSMS, msg.Type)
+ assert.Equal(t, receiver1.ID, msg.ReceiverID)
+ assert.Equal(t, wallet1.ID, msg.WalletID)
+ assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID)
+ assert.Equal(t, data.FailureMessageStatus, msg.Status)
+ assert.Equal(t, titleWallet1, msg.TitleEncrypted)
+ assert.Equal(t, contentWallet1, msg.TextEncrypted)
+ assert.Len(t, msg.StatusHistory, 2)
+ assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status)
+ assert.Equal(t, data.FailureMessageStatus, msg.StatusHistory[1].Status)
+ assert.Nil(t, msg.AssetID)
+
+ msg = data.Message{}
+ err = dbConnectionPool.GetContext(ctx, &msg, q, receiver2.ID, wallet2.ID, rec2RW.ID)
+ require.NoError(t, err)
+
+ assert.Equal(t, message.MessengerTypeTwilioSMS, msg.Type)
+ assert.Equal(t, receiver2.ID, msg.ReceiverID)
+ assert.Equal(t, wallet2.ID, msg.WalletID)
+ assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID)
+ assert.Equal(t, data.SuccessMessageStatus, msg.Status)
+ assert.Equal(t, titleWallet2, msg.TitleEncrypted)
+ assert.Equal(t, contentWallet2, msg.TextEncrypted)
+ assert.Len(t, msg.StatusHistory, 2)
+ assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status)
+ assert.Equal(t, data.SuccessMessageStatus, msg.StatusHistory[1].Status)
+ assert.Nil(t, msg.AssetID)
}
diff --git a/internal/serve/httphandler/countries_handler.go b/internal/serve/httphandler/countries_handler.go
deleted file mode 100644
index 565728d3c..000000000
--- a/internal/serve/httphandler/countries_handler.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package httphandler
-
-import (
- "net/http"
-
- "github.com/stellar/go/support/render/httpjson"
-
- "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
- "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror"
-)
-
-type CountriesHandler struct {
- Models *data.Models
-}
-
-// GetCountries returns a list of countries
-func (c CountriesHandler) GetCountries(w http.ResponseWriter, r *http.Request) {
- countries, err := c.Models.Countries.GetAll(r.Context())
- if err != nil {
- ctx := r.Context()
- httperror.InternalError(ctx, "Cannot retrieve countries", err, nil).Render(w)
- return
- }
- httpjson.Render(w, countries, httpjson.JSON)
-}
diff --git a/internal/serve/httphandler/countries_handler_test.go b/internal/serve/httphandler/countries_handler_test.go
deleted file mode 100644
index 96a1e3cf9..000000000
--- a/internal/serve/httphandler/countries_handler_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package httphandler
-
-import (
- "context"
- "encoding/json"
- "io"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/stellar/stellar-disbursement-platform-backend/db"
- "github.com/stellar/stellar-disbursement-platform-backend/db/dbtest"
- "github.com/stellar/stellar-disbursement-platform-backend/internal/data"
-)
-
-func Test_CountriesHandlerGetCountries(t *testing.T) {
- dbt := dbtest.Open(t)
- defer dbt.Close()
-
- dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
- require.NoError(t, err)
- defer dbConnectionPool.Close()
-
- models, err := data.NewModels(dbConnectionPool)
- require.NoError(t, err)
-
- ctx := context.Background()
-
- handler := &CountriesHandler{
- Models: models,
- }
-
- t.Run("successfully returns a list of countries", func(t *testing.T) {
- expected := data.ClearAndCreateCountryFixtures(t, ctx, dbConnectionPool)
- expectedJSON, err := json.Marshal(expected)
- require.NoError(t, err)
-
- rr := httptest.NewRecorder()
- req, _ := http.NewRequest("GET", "/countries", nil)
- http.HandlerFunc(handler.GetCountries).ServeHTTP(rr, req)
-
- resp := rr.Result()
-
- respBody, err := io.ReadAll(resp.Body)
- require.NoError(t, err)
-
- assert.Equal(t, http.StatusOK, resp.StatusCode)
-
- assert.JSONEq(t, string(expectedJSON), string(respBody))
- })
-}
diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go
index 14eb67b02..c17f3f2ba 100644
--- a/internal/serve/httphandler/disbursement_handler.go
+++ b/internal/serve/httphandler/disbursement_handler.go
@@ -43,7 +43,6 @@ 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"`
VerificationField data.VerificationType `json:"verification_field"`
@@ -55,7 +54,6 @@ func (d DisbursementHandler) validateRequest(req PostDisbursementRequest) *valid
v := validators.NewValidator()
v.Check(req.Name != "", "name", "name is required")
- v.Check(req.CountryCode != "", "country_code", "country_code is required")
v.Check(req.WalletID != "", "wallet_id", "wallet_id is required")
v.Check(req.AssetID != "", "asset_id", "asset_id is required")
v.Check(
@@ -122,17 +120,9 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req
return
}
- // Get Country
- country, err := d.Models.Countries.Get(ctx, req.CountryCode)
- if err != nil {
- httperror.BadRequest("country code could not be retrieved", err, nil).Render(w)
- return
- }
-
// Insert disbursement
disbursement := data.Disbursement{
Asset: asset,
- Country: country,
Name: req.Name,
ReceiverRegistrationMessageTemplate: req.ReceiverRegistrationMessageTemplate,
RegistrationContactType: req.RegistrationContactType,
@@ -163,9 +153,8 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req
// Monitor disbursement creation
labels := monitor.DisbursementLabels{
- Asset: newDisbursement.Asset.Code,
- Country: newDisbursement.Country.Code,
- Wallet: newDisbursement.Wallet.Name,
+ Asset: newDisbursement.Asset.Code,
+ Wallet: newDisbursement.Wallet.Name,
}
err = d.MonitorService.MonitorCounters(monitor.DisbursementsCounterTag, labels.ToMap())
if err != nil {
diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go
index ede0121b3..d8892602b 100644
--- a/internal/serve/httphandler/disbursement_handler_test.go
+++ b/internal/serve/httphandler/disbursement_handler_test.go
@@ -48,7 +48,6 @@ func Test_DisbursementHandler_validateRequest(t *testing.T) {
request: PostDisbursementRequest{},
expectedErrors: map[string]interface{}{
"name": "name is required",
- "country_code": "country_code is required",
"wallet_id": "wallet_id is required",
"asset_id": "asset_id is required",
"registration_contact_type": fmt.Sprintf("registration_contact_type must be one of %v", data.AllRegistrationContactTypes()),
@@ -58,10 +57,9 @@ func Test_DisbursementHandler_validateRequest(t *testing.T) {
{
name: "๐ด registration_contact_type and verification_field are invalid",
request: PostDisbursementRequest{
- Name: "disbursement 1",
- CountryCode: "UKR",
- AssetID: "61dbfa89-943a-413c-b862-a2177384d321",
- WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
+ Name: "disbursement 1",
+ AssetID: "61dbfa89-943a-413c-b862-a2177384d321",
+ WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
RegistrationContactType: data.RegistrationContactType{
ReceiverContactType: "invalid1",
},
@@ -76,7 +74,6 @@ func Test_DisbursementHandler_validateRequest(t *testing.T) {
name: "๐ข all fields are valid",
request: PostDisbursementRequest{
Name: "disbursement 1",
- CountryCode: "UKR",
AssetID: "61dbfa89-943a-413c-b862-a2177384d321",
WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
RegistrationContactType: data.RegistrationContactTypePhone,
@@ -124,13 +121,11 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
enabledWallet.Assets = nil
asset := data.GetAssetFixture(t, ctx, dbConnectionPool, data.FixtureAssetUSDC)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUKR)
existingDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "existing disbursement",
- Asset: asset,
- Wallet: &enabledWallet,
- Country: country,
+ Name: "existing disbursement",
+ Asset: asset,
+ Wallet: &enabledWallet,
})
type TestCase struct {
@@ -149,7 +144,6 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
"error": "The request was invalid in some way.",
"extras": {
"name": "name is required",
- "country_code": "country_code is required",
"wallet_id": "wallet_id is required",
"asset_id": "asset_id is required",
"registration_contact_type": "registration_contact_type must be one of [EMAIL EMAIL_AND_WALLET_ADDRESS PHONE_NUMBER PHONE_NUMBER_AND_WALLET_ADDRESS]",
@@ -162,7 +156,6 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
name: "๐ด wallet_id could not be found",
reqBody: map[string]interface{}{
"name": "disbursement 1",
- "country_code": country.Code,
"asset_id": asset.ID,
"wallet_id": "not-found-wallet-id",
"registration_contact_type": data.RegistrationContactTypePhone,
@@ -177,7 +170,6 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
name: "๐ด wallet is not enabled",
reqBody: map[string]interface{}{
"name": "disbursement 1",
- "country_code": country.Code,
"asset_id": asset.ID,
"wallet_id": disabledWallet.ID,
"registration_contact_type": data.RegistrationContactTypePhone,
@@ -192,7 +184,6 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
name: "๐ด asset_id could not be found",
reqBody: map[string]interface{}{
"name": "disbursement 1",
- "country_code": country.Code,
"asset_id": "not-found-asset-id",
"wallet_id": enabledWallet.ID,
"registration_contact_type": data.RegistrationContactTypePhone,
@@ -203,26 +194,10 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
return `{"error":"asset ID could not be retrieved"}`
},
},
- {
- name: "๐ด country_code could not be found",
- reqBody: map[string]interface{}{
- "name": "disbursement 1",
- "country_code": "not-found-country-code",
- "asset_id": asset.ID,
- "wallet_id": enabledWallet.ID,
- "registration_contact_type": data.RegistrationContactTypePhone,
- "verification_field": data.VerificationTypeDateOfBirth,
- },
- wantStatusCode: http.StatusBadRequest,
- wantResponseBodyFn: func(d *data.Disbursement) string {
- return `{"error":"country code could not be retrieved"}`
- },
- },
{
name: "๐ด non-unique disbursement name",
reqBody: map[string]interface{}{
"name": existingDisbursement.Name,
- "country_code": country.Code,
"asset_id": asset.ID,
"wallet_id": enabledWallet.ID,
"registration_contact_type": data.RegistrationContactTypePhone,
@@ -248,15 +223,13 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
name: fmt.Sprintf("๐ข[%s]registration_contact_type%s", registrationContactType, testNameSuffix),
prepareMocksFn: func(t *testing.T, mMonitorService *monitorMocks.MockMonitorService) {
labels := monitor.DisbursementLabels{
- Asset: asset.Code,
- Country: country.Code,
- Wallet: enabledWallet.Name,
+ Asset: asset.Code,
+ Wallet: enabledWallet.Name,
}
mMonitorService.On("MonitorCounters", monitor.DisbursementsCounterTag, labels.ToMap()).Return(nil).Once()
},
reqBody: map[string]interface{}{
"name": fmt.Sprintf("successful disbursement %d", i),
- "country_code": country.Code,
"asset_id": asset.ID,
"wallet_id": enabledWallet.ID,
"registration_contact_type": registrationContactType.String(),
@@ -289,12 +262,6 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) {
"updated_at": asset.UpdatedAt.Format(time.RFC3339Nano),
"deleted_at": nil,
},
- "country": map[string]interface{}{
- "code": country.Code,
- "name": country.Name,
- "created_at": country.CreatedAt.Format(time.RFC3339Nano),
- "updated_at": country.UpdatedAt.Format(time.RFC3339Nano),
- },
"wallet": map[string]interface{}{
"id": enabledWallet.ID,
"name": enabledWallet.Name,
@@ -488,7 +455,6 @@ func Test_DisbursementHandler_GetDisbursements_Success(t *testing.T) {
// create fixtures
wallet := data.CreateDefaultWalletFixture(t, ctx, dbConnectionPool)
asset := data.GetAssetFixture(t, ctx, dbConnectionPool, data.FixtureAssetUSDC)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUKR)
createdByUser := auth.User{
ID: "User1",
@@ -551,7 +517,6 @@ func Test_DisbursementHandler_GetDisbursements_Success(t *testing.T) {
StatusHistory: draftStatusHistory,
Asset: asset,
Wallet: wallet,
- Country: country,
CreatedAt: time.Date(2022, 3, 21, 23, 40, 20, 1431, time.UTC),
})
disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
@@ -560,7 +525,6 @@ func Test_DisbursementHandler_GetDisbursements_Success(t *testing.T) {
StatusHistory: draftStatusHistory,
Asset: asset,
Wallet: wallet,
- Country: country,
CreatedAt: time.Date(2023, 2, 20, 23, 40, 20, 1431, time.UTC),
})
disbursement3 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
@@ -569,7 +533,6 @@ func Test_DisbursementHandler_GetDisbursements_Success(t *testing.T) {
StatusHistory: startedStatusHistory,
Asset: asset,
Wallet: wallet,
- Country: country,
CreatedAt: time.Date(2023, 3, 19, 23, 40, 20, 1431, time.UTC),
})
disbursement4 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
@@ -578,7 +541,6 @@ func Test_DisbursementHandler_GetDisbursements_Success(t *testing.T) {
StatusHistory: draftStatusHistory,
Asset: asset,
Wallet: wallet,
- Country: country,
CreatedAt: time.Date(2023, 4, 19, 23, 40, 20, 1431, time.UTC),
})
@@ -871,14 +833,12 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
// create fixtures
wallet := data.CreateDefaultWalletFixture(t, ctx, dbConnectionPool)
asset := data.GetAssetFixture(t, ctx, dbConnectionPool, data.FixtureAssetUSDC)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUKR)
// create disbursement
draftDisbursement := data.CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, handler.Models.Disbursements, data.Disbursement{
- Name: "disbursement1",
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Name: "disbursement1",
+ Asset: asset,
+ Wallet: wallet,
})
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, handler.Models.Disbursements, &data.Disbursement{
@@ -1212,24 +1172,19 @@ func Test_DisbursementHandler_GetDisbursementReceivers(t *testing.T) {
asset := data.CreateAssetFixture(t, context.Background(), dbConnectionPool,
"USDC",
"GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, context.Background(), dbConnectionPool,
- "FRA",
- "France")
// create disbursements
disbursementWithReceivers := data.CreateDisbursementFixture(t, context.Background(), dbConnectionPool, handler.Models.Disbursements, &data.Disbursement{
- Name: "disbursement with receivers",
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement with receivers",
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
disbursementWithoutReceivers := data.CreateDisbursementFixture(t, context.Background(), dbConnectionPool, handler.Models.Disbursements, &data.Disbursement{
- Name: "disbursement without receivers",
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement without receivers",
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// create disbursement receivers
diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go
index fcad3f450..f7571ea33 100644
--- a/internal/serve/httphandler/payments_handler_test.go
+++ b/internal/serve/httphandler/payments_handler_test.go
@@ -65,18 +65,16 @@ func Test_PaymentsHandlerGet(t *testing.T) {
ctx := context.Background()
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.DraftReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
stellarTransactionID, err := utils.RandomString(64)
@@ -200,13 +198,11 @@ func Test_PaymentHandler_GetPayments_CirclePayments(t *testing.T) {
// Create fixtures
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet,
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
})
receiverReady := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
rwReady := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverReady.ID, wallet.ID, data.ReadyReceiversWalletStatus)
@@ -469,7 +465,6 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) {
// create fixtures
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
// create receivers
@@ -481,19 +476,17 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) {
// create disbursements
disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 2",
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 2",
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
stellarTransactionID, err := utils.RandomString(64)
@@ -766,7 +759,6 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) {
func Test_PaymentHandler_RetryPayments(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -776,18 +768,8 @@ func Test_PaymentHandler_RetryPayments(t *testing.T) {
tnt := tenant.Tenant{ID: "tenant-id"}
- ctx := context.Background()
- ctx = tenant.SaveTenantInContext(ctx, &tnt)
-
- data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
+ ctx := tenant.SaveTenantInContext(context.Background(), &tnt)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -795,7 +777,6 @@ func Test_PaymentHandler_RetryPayments(t *testing.T) {
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -1480,18 +1461,16 @@ func Test_PaymentsHandler_getPaymentsWithCount(t *testing.T) {
})
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.DraftReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -1569,15 +1548,13 @@ func Test_PaymentsHandler_PatchPaymentStatus(t *testing.T) {
// create fixtures
wallet := data.CreateDefaultWalletFixture(t, ctx, dbConnectionPool)
asset := data.GetAssetFixture(t, ctx, dbConnectionPool, data.FixtureAssetUSDC)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUSA)
// create disbursements
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "ready disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "ready disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// create disbursement receivers
diff --git a/internal/serve/httphandler/receiver_handler_test.go b/internal/serve/httphandler/receiver_handler_test.go
index 507ef66d5..92db1e072 100644
--- a/internal/serve/httphandler/receiver_handler_test.go
+++ b/internal/serve/httphandler/receiver_handler_test.go
@@ -44,15 +44,13 @@ func Test_ReceiverHandlerGet(t *testing.T) {
ctx := context.Background()
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet1 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet1.com", "www.wallet1.com", "wallet1://")
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
disbursement := data.Disbursement{
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Country: country,
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
}
stellarTransactionID, err := utils.RandomString(64)
@@ -488,7 +486,6 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
// create fixtures
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
// create receivers
@@ -596,11 +593,10 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) {
// create disbursements
disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
stellarTransactionID, err := utils.RandomString(64)
diff --git a/internal/serve/httphandler/statistics_handler_test.go b/internal/serve/httphandler/statistics_handler_test.go
index dd7bc83fa..4fff57490 100644
--- a/internal/serve/httphandler/statistics_handler_test.go
+++ b/internal/serve/httphandler/statistics_handler_test.go
@@ -88,15 +88,13 @@ func TestStatisticsHandler(t *testing.T) {
require.NoError(t, err)
asset1 := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.CompletedDisbursementStatus,
- Asset: asset1,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.CompletedDisbursementStatus,
+ Asset: asset1,
+ Wallet: wallet,
})
t.Run("get statistics for existing disbursement with no data", func(t *testing.T) {
diff --git a/internal/serve/httphandler/verify_receiver_registration_handler_test.go b/internal/serve/httphandler/verify_receiver_registration_handler_test.go
index 040cf985e..b15ea78c4 100644
--- a/internal/serve/httphandler/verify_receiver_registration_handler_test.go
+++ b/internal/serve/httphandler/verify_receiver_registration_handler_test.go
@@ -585,7 +585,6 @@ func Test_VerifyReceiverRegistrationHandler_buildPaymentsReadyToPayEventMessage(
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://home.page", "home.page", "wallet123://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "UKR", "Ukraine")
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
rw := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
@@ -598,10 +597,9 @@ func Test_VerifyReceiverRegistrationHandler_buildPaymentsReadyToPayEventMessage(
}
pausedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Wallet: wallet,
- Asset: asset,
- Country: country,
- Status: data.PausedDisbursementStatus,
+ Wallet: wallet,
+ Asset: asset,
+ Status: data.PausedDisbursementStatus,
})
_ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -638,10 +636,9 @@ func Test_VerifyReceiverRegistrationHandler_buildPaymentsReadyToPayEventMessage(
}
disbursement := data.CreateDisbursementFixture(t, ctxWithoutTenant, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Wallet: wallet,
- Asset: asset,
- Country: country,
- Status: data.StartedDisbursementStatus,
+ Wallet: wallet,
+ Asset: asset,
+ Status: data.StartedDisbursementStatus,
})
_ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -672,10 +669,9 @@ func Test_VerifyReceiverRegistrationHandler_buildPaymentsReadyToPayEventMessage(
}
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Wallet: wallet,
- Asset: asset,
- Country: country,
- Status: data.StartedDisbursementStatus,
+ Wallet: wallet,
+ Asset: asset,
+ Status: data.StartedDisbursementStatus,
})
payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -721,10 +717,9 @@ func Test_VerifyReceiverRegistrationHandler_buildPaymentsReadyToPayEventMessage(
}
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Wallet: wallet,
- Asset: asset,
- Country: country,
- Status: data.StartedDisbursementStatus,
+ Wallet: wallet,
+ Asset: asset,
+ Status: data.StartedDisbursementStatus,
})
payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -1330,7 +1325,6 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin
// update database with the entries needed
defer data.DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- defer data.DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
defer data.DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
defer data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool)
@@ -1349,12 +1343,10 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin
// Creating a payment ready to pay
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "UKR", "Ukraine")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Wallet: wallet,
- Asset: asset,
- Country: country,
- Status: data.StartedDisbursementStatus,
+ Wallet: wallet,
+ Asset: asset,
+ Status: data.StartedDisbursementStatus,
})
payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
Amount: "100",
diff --git a/internal/serve/serve.go b/internal/serve/serve.go
index 54e4dd54f..6f37cb534 100644
--- a/internal/serve/serve.go
+++ b/internal/serve/serve.go
@@ -324,10 +324,6 @@ func handleHTTP(o ServeOptions) *chi.Mux {
Patch("/wallets/{receiver_wallet_id}", receiverWalletHandler.RetryInvitation)
})
- r.With(middleware.AnyRoleMiddleware(authManager, data.GetAllRoles()...)).Route("/countries", func(r chi.Router) {
- r.Get("/", httphandler.CountriesHandler{Models: o.Models}.GetCountries)
- })
-
r.
With(middleware.AnyRoleMiddleware(authManager, data.GetAllRoles()...)).
Get("/registration-contact-types", httphandler.RegistrationContactTypesHandler{}.Get)
diff --git a/internal/serve/serve_test.go b/internal/serve/serve_test.go
index e25a48a6b..84e60ff33 100644
--- a/internal/serve/serve_test.go
+++ b/internal/serve/serve_test.go
@@ -454,8 +454,6 @@ func Test_handleHTTP_authenticatedEndpoints(t *testing.T) {
{http.MethodPatch, "/receivers/1234"},
{http.MethodPatch, "/receivers/wallets/1234"},
{http.MethodGet, "/receivers/verification-types"},
- // Countries
- {http.MethodGet, "/countries"},
// Receiver Contact Types
{http.MethodGet, "/registration-contact-types"},
// Assets
diff --git a/internal/services/circle_reconciliation_service_test.go b/internal/services/circle_reconciliation_service_test.go
index c89549b67..26cd2e008 100644
--- a/internal/services/circle_reconciliation_service_test.go
+++ b/internal/services/circle_reconciliation_service_test.go
@@ -165,7 +165,6 @@ func Test_NewCircleReconciliationService_Reconcile_partialSuccess(t *testing.T)
require.NoError(t, err)
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, assets.EURCAssetCode, assets.EURCAssetTestnet.Issuer)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "My Wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
// Create distribution accounts
@@ -176,11 +175,10 @@ func Test_NewCircleReconciliationService_Reconcile_partialSuccess(t *testing.T)
}
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
@@ -332,15 +330,13 @@ func Test_NewCircleReconciliationService_reconcileTransferRequest(t *testing.T)
require.NoError(t, err)
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, assets.EURCAssetCode, assets.EURCAssetTestnet.Issuer)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "My Wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
diff --git a/internal/services/disbursement_management_service_test.go b/internal/services/disbursement_management_service_test.go
index 9ca0fc2b9..7418f7305 100644
--- a/internal/services/disbursement_management_service_test.go
+++ b/internal/services/disbursement_management_service_test.go
@@ -210,7 +210,6 @@ func Test_DisbursementManagementService_StartDisbursement_success(t *testing.T)
// Create fixtures: asset, wallet, country
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, assets.EURCAssetCode, assets.EURCAssetIssuerTestnet)
wallet := data.CreateDefaultWalletFixture(t, ctx, dbConnectionPool)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUKR)
// Update context with tenant and auth token
tnt := tenant.Tenant{ID: "tenant-id"}
@@ -337,11 +336,10 @@ func Test_DisbursementManagementService_StartDisbursement_success(t *testing.T)
// Create fixtures: disbursements
readyDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "ready disbursement",
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "ready disbursement",
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
StatusHistory: []data.DisbursementStatusHistoryEntry{
{UserID: ownerUser.ID, Status: data.DraftDisbursementStatus},
{UserID: ownerUser.ID, Status: data.ReadyDisbursementStatus},
@@ -514,15 +512,13 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
// create fixtures
wallet := data.CreateDefaultWalletFixture(t, ctx, dbConnectionPool)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUKR)
// Create fixtures: disbursements
draftDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "draft disbursement",
- Status: data.DraftDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "draft disbursement",
+ Status: data.DraftDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// Create fixtures: receivers, receiver wallets
@@ -572,11 +568,10 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
userID := "9ae68f09-cad9-4311-9758-4ff59d2e9e6d"
disbursement := data.CreateDisbursementFixture(t, context.Background(), dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement #1",
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement #1",
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
StatusHistory: []data.DisbursementStatusHistoryEntry{
{
Status: data.DraftDisbursementStatus,
@@ -612,11 +607,10 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
usdt := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDT", "GBVHJTRLQRMIHRYTXZQOPVYCVVH7IRJN3DOFT7VC6U75CBWWBVDTWURG")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement - balance insufficient",
- Status: data.StartedDisbursementStatus,
- Asset: usdt,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement - balance insufficient",
+ Status: data.StartedDisbursementStatus,
+ Asset: usdt,
+ Wallet: wallet,
})
// should consider this payment since it's the same asset
data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -628,11 +622,10 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
})
disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement #4",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement #4",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// should NOT consider this payment since it's NOT the same asset
data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -644,11 +637,10 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
})
disbursementInsufficientBalance := data.CreateDisbursementFixture(t, context.Background(), dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement - insufficient balance",
- Status: data.ReadyDisbursementStatus,
- Asset: usdt,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement - insufficient balance",
+ Status: data.ReadyDisbursementStatus,
+ Asset: usdt,
+ Wallet: wallet,
})
data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
ReceiverWallet: rwReady,
@@ -724,7 +716,6 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
Status: data.ReadyDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
StatusHistory: statusHistory,
})
@@ -829,7 +820,6 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
Status: data.ReadyDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
StatusHistory: statusHistory,
})
@@ -907,7 +897,6 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
Status: data.ReadyDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
StatusHistory: statusHistory,
})
@@ -961,7 +950,6 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T)
Status: data.ReadyDisbursementStatus,
Asset: asset,
Wallet: wallet,
- Country: country,
StatusHistory: statusHistory,
})
@@ -1089,23 +1077,20 @@ func Test_DisbursementManagementService_PauseDisbursement(t *testing.T) {
// create fixtures
wallet := data.CreateDefaultWalletFixture(t, ctx, dbConnectionPool)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUSA)
// create disbursements
readyDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "ready disbursement",
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "ready disbursement",
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "started disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "started disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// create disbursement receivers
@@ -1354,15 +1339,13 @@ func Test_DisbursementManagementService_validateBalanceForDisbursement(t *testin
models, outerErr := data.NewModels(dbConnectionPool)
require.NoError(t, outerErr)
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiverReady := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
rwReady := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverReady.ID, wallet.ID, data.ReadyReceiversWalletStatus)
disbursementOld := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet,
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
})
_ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
ReceiverWallet: rwReady,
@@ -1372,10 +1355,9 @@ func Test_DisbursementManagementService_validateBalanceForDisbursement(t *testin
Status: data.PendingPaymentStatus,
})
disbursementNew := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet,
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
})
_ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
ReceiverWallet: rwReady,
diff --git a/internal/services/patch_anchor_platform_transactions_completion_test.go b/internal/services/patch_anchor_platform_transactions_completion_test.go
index 8f52101cc..544703ab8 100644
--- a/internal/services/patch_anchor_platform_transactions_completion_test.go
+++ b/internal/services/patch_anchor_platform_transactions_completion_test.go
@@ -66,7 +66,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
t.Run("doesn't patch the transaction when payment isn't on Success or Failed status", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -74,7 +73,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -103,7 +101,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
t.Run("doesn't mark as synced when fails patching anchor platform transaction when payment is success", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -111,7 +108,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -174,7 +170,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
t.Run("mark as synced when patch anchor platform transaction successfully and payment is failed", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -182,7 +177,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -228,7 +222,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
t.Run("marks as synced when patch anchor platform transaction successfully and payment is success", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -236,7 +229,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -293,7 +285,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
t.Run("marks as synced when patch anchor platform transaction successfully and payment is success (XLM)", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "XLM", "")
@@ -301,7 +292,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -358,7 +348,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
t.Run("doesn't patch the transaction when it's already patch as completed", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -366,7 +355,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -442,7 +430,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
t.Run("doesn't mark as synced when fails patching anchor platform transaction when payment is success", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -450,7 +437,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -506,7 +492,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
t.Run("mark as synced when patch anchor platform transaction successfully and payment is failed", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -514,7 +499,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -566,7 +550,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
t.Run("marks as synced when patch anchor platform transaction successfully and payment is success", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -574,7 +557,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -629,7 +611,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
t.Run("doesn't patch the transaction when it's already patch as completed", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -637,7 +618,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -645,7 +625,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
})
disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -713,7 +692,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
t.Run("patches the transactions successfully if the other payments were failed", func(t *testing.T) {
data.DeleteAllFixtures(t, ctx, dbConnectionPool)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -721,7 +699,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -729,7 +706,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
})
disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
@@ -737,7 +713,6 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor
})
disbursement3 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.StartedDisbursementStatus,
diff --git a/internal/services/payment_from_submitter_service_test.go b/internal/services/payment_from_submitter_service_test.go
index 5b7c493cc..c009bb13c 100644
--- a/internal/services/payment_from_submitter_service_test.go
+++ b/internal/services/payment_from_submitter_service_test.go
@@ -63,17 +63,13 @@ func Test_PaymentFromSubmitterService_SyncBatchTransactions(t *testing.T) {
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool,
"USDC",
"GABC65XJDMXTGPNZRCI6V3KOKKWVK55UEKGQLONRIVYPMEJNNQ45YOEE")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool,
- "FRA",
- "France")
// create disbursements
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, testCtx.sdpModel.Disbursements, &data.Disbursement{
- Name: "ready disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "ready disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// create disbursement receivers
@@ -270,17 +266,13 @@ func Test_PaymentFromSubmitterService_SyncTransaction(t *testing.T) {
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool,
"USDC",
"GABC65XJDMXTGPNZRCI6V3KOKKWVK55UEKGQLONRIVYPMEJNNQ45YOEE")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool,
- "FRA",
- "France")
// create disbursements
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, testCtx.sdpModel.Disbursements, &data.Disbursement{
- Name: "ready disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "ready disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// create disbursement receivers
@@ -578,7 +570,6 @@ func updateTSSTransactionsToError(t *testing.T, testCtx *testContext, txDataSlic
func Test_PaymentFromSubmitterService_RetryingPayment(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, outerErr := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, outerErr)
defer dbConnectionPool.Close()
@@ -588,17 +579,7 @@ func Test_PaymentFromSubmitterService_RetryingPayment(t *testing.T) {
monitorService := NewPaymentFromSubmitterService(testCtx.sdpModel, dbConnectionPool)
- // clean test db
- data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
-
// create fixtures
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GABC65XJDMXTGPNZRCI6V3KOKKWVK55UEKGQLONRIVYPMEJNNQ45YOEE")
@@ -606,11 +587,10 @@ func Test_PaymentFromSubmitterService_RetryingPayment(t *testing.T) {
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, testCtx.sdpModel.Disbursements, &data.Disbursement{
- Name: "started disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "started disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, testCtx.sdpModel.Payment, &data.Payment{
@@ -704,7 +684,6 @@ func Test_PaymentFromSubmitterService_RetryingPayment(t *testing.T) {
func Test_PaymentFromSubmitterService_CompleteDisbursements(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, outerErr := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, outerErr)
defer dbConnectionPool.Close()
@@ -714,17 +693,7 @@ func Test_PaymentFromSubmitterService_CompleteDisbursements(t *testing.T) {
monitorService := NewPaymentFromSubmitterService(testCtx.sdpModel, dbConnectionPool)
- // clean test db
- data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
-
// create fixtures
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GABC65XJDMXTGPNZRCI6V3KOKKWVK55UEKGQLONRIVYPMEJNNQ45YOEE")
@@ -732,11 +701,10 @@ func Test_PaymentFromSubmitterService_CompleteDisbursements(t *testing.T) {
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, testCtx.sdpModel.Disbursements, &data.Disbursement{
- Name: "started disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "started disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, testCtx.sdpModel.Payment, &data.Payment{
diff --git a/internal/services/payment_management_service_test.go b/internal/services/payment_management_service_test.go
index dce483945..7b61447e0 100644
--- a/internal/services/payment_management_service_test.go
+++ b/internal/services/payment_management_service_test.go
@@ -31,15 +31,13 @@ func Test_PaymentManagementService_CancelPayment(t *testing.T) {
// create fixtures
wallet := data.CreateDefaultWalletFixture(t, ctx, dbConnectionPool)
asset := data.GetAssetFixture(t, ctx, dbConnectionPool, data.FixtureAssetUSDC)
- country := data.GetCountryFixture(t, ctx, dbConnectionPool, data.FixtureCountryUSA)
// create disbursements
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "ready disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "ready disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
// create disbursement receivers
diff --git a/internal/services/payment_to_submitter_service_test.go b/internal/services/payment_to_submitter_service_test.go
index 8fa2fc734..6dbcc1c43 100644
--- a/internal/services/payment_to_submitter_service_test.go
+++ b/internal/services/payment_to_submitter_service_test.go
@@ -39,7 +39,6 @@ func Test_PaymentToSubmitterService_SendPaymentsMethods(t *testing.T) {
eurcAsset := data.CreateAssetFixture(t, ctx, dbConnectionPool, assets.EURCAssetCode, assets.EURCAssetTestnet.Issuer)
nativeAsset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "XLM", "")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "My Wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
models, err := data.NewModels(dbConnectionPool)
@@ -129,11 +128,10 @@ func Test_PaymentToSubmitterService_SendPaymentsMethods(t *testing.T) {
defer data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "ready disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: tc.asset,
- Wallet: wallet,
- Country: country,
+ Name: "ready disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: tc.asset,
+ Wallet: wallet,
})
receiverReady := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
@@ -446,7 +444,6 @@ func Test_PaymentToSubmitterService_ValidatePaymentReadyForSending(t *testing.T)
func Test_PaymentToSubmitterService_RetryPayment(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -472,17 +469,7 @@ func Test_PaymentToSubmitterService_RetryPayment(t *testing.T) {
PaymentDispatcher: paymentDispatcher,
})
- // clean test db
- data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
-
// create fixtures
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GDUCE34WW5Z34GMCEPURYANUCUP47J6NORJLKC6GJNMDLN4ZI4PMI2MG")
@@ -490,11 +477,10 @@ func Test_PaymentToSubmitterService_RetryPayment(t *testing.T) {
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "started disbursement",
- Status: data.StartedDisbursementStatus,
- Asset: asset,
- Wallet: wallet,
- Country: country,
+ Name: "started disbursement",
+ Status: data.StartedDisbursementStatus,
+ Asset: asset,
+ Wallet: wallet,
})
payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{
@@ -592,13 +578,11 @@ func Test_PaymentToSubmitterService_markPaymentsAsFailed(t *testing.T) {
models, err := data.NewModels(dbConnectionPool)
require.NoError(t, err)
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet,
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
})
receiverReady := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
rwReady := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverReady.ID, wallet.ID, data.ReadyReceiversWalletStatus)
diff --git a/internal/services/ready_payments_cancelation_service_test.go b/internal/services/ready_payments_cancelation_service_test.go
index 834ea3622..218f5252c 100644
--- a/internal/services/ready_payments_cancelation_service_test.go
+++ b/internal/services/ready_payments_cancelation_service_test.go
@@ -17,7 +17,6 @@ import (
func Test_ReadyPaymentsCancellationService_CancelReadyPaymentsService(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -28,15 +27,6 @@ func Test_ReadyPaymentsCancellationService_CancelReadyPaymentsService(t *testing
service := NewReadyPaymentsCancellationService(models)
ctx := context.Background()
- data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllCountryFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllAssetFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
- data.DeleteAllWalletFixtures(t, ctx, dbConnectionPool)
-
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "BRA", "Brazil")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
@@ -44,7 +34,6 @@ func Test_ReadyPaymentsCancellationService_CancelReadyPaymentsService(t *testing
receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus)
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet,
Asset: asset,
Status: data.ReadyDisbursementStatus,
diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go
index 2115db7b3..b71b9f5ba 100644
--- a/internal/services/send_receiver_wallets_invite_service_test.go
+++ b/internal/services/send_receiver_wallets_invite_service_test.go
@@ -71,8 +71,6 @@ func Test_SendReceiverWalletInviteService_SendInvite(t *testing.T) {
models, err := data.NewModels(dbConnectionPool)
require.NoError(t, err)
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "ATL", "Atlantis")
-
wallet1 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet1", "https://wallet1.com", "www.wallet1.com", "wallet1://sdp")
wallet2 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "Wallet2", "https://wallet2.com", "www.wallet2.com", "wallet2://sdp")
@@ -89,17 +87,15 @@ func Test_SendReceiverWalletInviteService_SendInvite(t *testing.T) {
})
disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet1,
- Status: data.ReadyDisbursementStatus,
- Asset: asset1,
+ Wallet: wallet1,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset1,
})
disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet2,
- Status: data.ReadyDisbursementStatus,
- Asset: asset2,
+ Wallet: wallet2,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset2,
})
t.Run("returns error when service has wrong setup", func(t *testing.T) {
@@ -797,7 +793,6 @@ func Test_SendReceiverWalletInviteService_SendInvite(t *testing.T) {
t.Run("send disbursement invite successfully", func(t *testing.T) {
disbursement3 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet1,
Status: data.ReadyDisbursementStatus,
Asset: asset1,
@@ -805,7 +800,6 @@ func Test_SendReceiverWalletInviteService_SendInvite(t *testing.T) {
})
disbursement4 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet2,
Status: data.ReadyDisbursementStatus,
Asset: asset2,
@@ -950,7 +944,6 @@ func Test_SendReceiverWalletInviteService_SendInvite(t *testing.T) {
t.Run("successfully resend the disbursement invitation SMS", func(t *testing.T) {
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
Wallet: wallet1,
Status: data.ReadyDisbursementStatus,
Asset: asset1,
diff --git a/internal/statistics/calculate_statistics_test.go b/internal/statistics/calculate_statistics_test.go
index 13b7f404a..f0739e9f0 100644
--- a/internal/statistics/calculate_statistics_test.go
+++ b/internal/statistics/calculate_statistics_test.go
@@ -97,7 +97,6 @@ func TestCalculateStatistics(t *testing.T) {
require.NoError(t, err)
asset1 := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
@@ -107,11 +106,10 @@ func TestCalculateStatistics(t *testing.T) {
receiverWallet2 := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet.ID, data.DraftReceiversWalletStatus)
disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 1",
- Status: data.CompletedDisbursementStatus,
- Asset: asset1,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 1",
+ Status: data.CompletedDisbursementStatus,
+ Asset: asset1,
+ Wallet: wallet,
})
stellarTransactionID, err := utils.RandomString(64)
@@ -220,11 +218,10 @@ func TestCalculateStatistics(t *testing.T) {
asset2 := data.CreateAssetFixture(t, ctx, dbConnectionPool, "EURT", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Name: "disbursement 2",
- Status: data.CompletedDisbursementStatus,
- Asset: asset2,
- Wallet: wallet,
- Country: country,
+ Name: "disbursement 2",
+ Status: data.CompletedDisbursementStatus,
+ Asset: asset2,
+ Wallet: wallet,
})
stellarTransactionID, err = utils.RandomString(64)
@@ -401,7 +398,6 @@ func Test_checkIfDisbursementExists(t *testing.T) {
t.Run("disbursement exists", func(t *testing.T) {
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, model.Disbursements, &data.Disbursement{
@@ -412,9 +408,8 @@ func Test_checkIfDisbursementExists(t *testing.T) {
UserID: "user1",
},
},
- Asset: asset,
- Country: country,
- Wallet: wallet,
+ Asset: asset,
+ Wallet: wallet,
})
exists, err := checkIfDisbursementExists(context.Background(), dbConnectionPool, disbursement.ID)
require.NoError(t, err)
diff --git a/stellar-multitenant/internal/httphandler/tenants_handler_test.go b/stellar-multitenant/internal/httphandler/tenants_handler_test.go
index 2011ff56d..8a8aa7901 100644
--- a/stellar-multitenant/internal/httphandler/tenants_handler_test.go
+++ b/stellar-multitenant/internal/httphandler/tenants_handler_test.go
@@ -348,7 +348,6 @@ func Test_TenantHandler_Post(t *testing.T) {
"auth_users",
"circle_client_config",
"circle_transfer_requests",
- "countries",
"disbursements",
"messages",
"organizations",
@@ -763,14 +762,12 @@ func Test_TenantHandler_Patch_error(t *testing.T) {
name: "400 response if attempting to deactivate a tenant with active payments",
initialStatus: tenant.ActivatedTenantStatus,
prepareMocksFn: func() {
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet,
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
})
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
rw := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.DraftReceiversWalletStatus)
diff --git a/stellar-multitenant/internal/provisioning/manager_test.go b/stellar-multitenant/internal/provisioning/manager_test.go
index e0e95f79c..be4e6ea75 100644
--- a/stellar-multitenant/internal/provisioning/manager_test.go
+++ b/stellar-multitenant/internal/provisioning/manager_test.go
@@ -455,7 +455,6 @@ func getExpectedTablesAfterMigrationsApplied() []string {
"auth_users",
"circle_client_config",
"circle_transfer_requests",
- "countries",
"disbursements",
"messages",
"organizations",
@@ -549,7 +548,6 @@ func Test_Manager_RollbackOnErrors(t *testing.T) {
// Needed for UpdateTenantConfig:
tStatus := tenant.ProvisionedTenantStatus
updatedTnt := tnt
- updatedTnt.DistributionAccountAddress = &distAccAddress
tntManagerMock.
On("UpdateTenantConfig", ctx, &tenant.TenantUpdate{
ID: updatedTnt.ID,
diff --git a/stellar-multitenant/internal/services/status_change_validator_test.go b/stellar-multitenant/internal/services/status_change_validator_test.go
index 85cd6fedf..48f5d58fb 100644
--- a/stellar-multitenant/internal/services/status_change_validator_test.go
+++ b/stellar-multitenant/internal/services/status_change_validator_test.go
@@ -73,14 +73,12 @@ func Test_ValidateStatus(t *testing.T) {
}).Return(&tenant.Tenant{ID: tntID, Status: tenant.ActivatedTenantStatus}, nil).Once()
},
createFixtures: func() {
- country := data.CreateCountryFixture(t, ctx, dbConnectionPool, "FRA", "France")
wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet://")
asset := data.CreateAssetFixture(t, ctx, dbConnectionPool, "USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVV")
disbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{
- Country: country,
- Wallet: wallet,
- Status: data.ReadyDisbursementStatus,
- Asset: asset,
+ Wallet: wallet,
+ Status: data.ReadyDisbursementStatus,
+ Asset: asset,
})
receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{})
rw := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.DraftReceiversWalletStatus)
From bc1384f4245fe59bf721a07c9cddd06ff0d8f6ea Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Mon, 4 Nov 2024 09:44:12 -0800
Subject: [PATCH 57/75] [SDP-1380] Allow disbursement.verification_field to be
empty (#456)
### What
Allow disbursement.verification_field to be empty
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1380
---
...ement-make-verification-field-nullable.sql | 7 ++++
internal/data/disbursements.go | 13 ++++---
internal/data/disbursements_test.go | 39 ++++++++++++++++---
.../serve/httphandler/disbursement_handler.go | 12 +++---
.../httphandler/disbursement_handler_test.go | 32 ++++++++++++++-
internal/utils/sql.go | 10 +++++
6 files changed, 94 insertions(+), 19 deletions(-)
create mode 100644 db/migrations/sdp-migrations/2024-11-01.1-disbursement-make-verification-field-nullable.sql
create mode 100644 internal/utils/sql.go
diff --git a/db/migrations/sdp-migrations/2024-11-01.1-disbursement-make-verification-field-nullable.sql b/db/migrations/sdp-migrations/2024-11-01.1-disbursement-make-verification-field-nullable.sql
new file mode 100644
index 000000000..fae89bea0
--- /dev/null
+++ b/db/migrations/sdp-migrations/2024-11-01.1-disbursement-make-verification-field-nullable.sql
@@ -0,0 +1,7 @@
+-- This migration drops the verification_field NOT NULL constraint from the disbursements table.
+
+-- +migrate Up
+ALTER TABLE disbursements
+ ALTER COLUMN verification_field DROP NOT NULL;
+
+-- +migrate Down
diff --git a/internal/data/disbursements.go b/internal/data/disbursements.go
index e2bef70c5..e82fbefd5 100644
--- a/internal/data/disbursements.go
+++ b/internal/data/disbursements.go
@@ -14,6 +14,7 @@ import (
"github.com/stellar/go/support/log"
"github.com/stellar/stellar-disbursement-platform-backend/db"
+ "github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
type Disbursement struct {
@@ -75,15 +76,15 @@ func (d *DisbursementModel) Insert(ctx context.Context, disbursement *Disburseme
VALUES
($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
- `
- var newId string
- err := d.dbConnectionPool.GetContext(ctx, &newId, q,
+ `
+ var newID string
+ err := d.dbConnectionPool.GetContext(ctx, &newID, q,
disbursement.Name,
disbursement.Status,
disbursement.StatusHistory,
disbursement.Wallet.ID,
disbursement.Asset.ID,
- disbursement.VerificationField,
+ utils.SQLNullString(string(disbursement.VerificationField)),
disbursement.ReceiverRegistrationMessageTemplate,
disbursement.RegistrationContactType,
)
@@ -95,7 +96,7 @@ func (d *DisbursementModel) Insert(ctx context.Context, disbursement *Disburseme
return "", fmt.Errorf("unable to create disbursement %s: %w", disbursement.Name, err)
}
- return newId, nil
+ return newID, nil
}
func (d *DisbursementModel) GetWithStatistics(ctx context.Context, id string) (*Disbursement, error) {
@@ -118,7 +119,7 @@ const selectDisbursementQuery = `
d.name,
d.status,
d.status_history,
- d.verification_field,
+ COALESCE(d.verification_field::text, '') as verification_field,
COALESCE(d.file_name, '') as file_name,
d.file_content,
d.created_at,
diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go
index efd51dd84..d7877666d 100644
--- a/internal/data/disbursements_test.go
+++ b/internal/data/disbursements_test.go
@@ -15,7 +15,6 @@ import (
func Test_DisbursementModelInsert(t *testing.T) {
dbt := dbtest.Open(t)
defer dbt.Close()
-
dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN)
require.NoError(t, err)
defer dbConnectionPool.Close()
@@ -31,7 +30,7 @@ func Test_DisbursementModelInsert(t *testing.T) {
smsTemplate := "You have a new payment waiting for you from org x. Click on the link to register."
disbursement := Disbursement{
- Name: "disbursement1",
+ Name: "disbursement",
Status: DraftDisbursementStatus,
StatusHistory: []DisbursementStatusHistoryEntry{
{
@@ -46,7 +45,9 @@ func Test_DisbursementModelInsert(t *testing.T) {
RegistrationContactType: RegistrationContactTypePhone,
}
- t.Run("returns error when disbursement already exists", func(t *testing.T) {
+ t.Run("๐ด fails to insert disbursements with non-unique name", func(t *testing.T) {
+ defer DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
+
_, err := disbursementModel.Insert(ctx, &disbursement)
require.NoError(t, err)
_, err = disbursementModel.Insert(ctx, &disbursement)
@@ -54,8 +55,9 @@ func Test_DisbursementModelInsert(t *testing.T) {
require.Equal(t, ErrRecordAlreadyExists, err)
})
- t.Run("insert disbursement successfully", func(t *testing.T) {
- disbursement.Name = "disbursement2"
+ t.Run("๐ข successfully insert disbursement", func(t *testing.T) {
+ defer DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
+
id, err := disbursementModel.Insert(ctx, &disbursement)
require.NoError(t, err)
require.NotNil(t, id)
@@ -63,7 +65,7 @@ func Test_DisbursementModelInsert(t *testing.T) {
actual, err := disbursementModel.Get(ctx, dbConnectionPool, id)
require.NoError(t, err)
- assert.Equal(t, "disbursement2", actual.Name)
+ assert.Equal(t, "disbursement", actual.Name)
assert.Equal(t, DraftDisbursementStatus, actual.Status)
assert.Equal(t, asset, actual.Asset)
assert.Equal(t, wallet, actual.Wallet)
@@ -73,6 +75,31 @@ func Test_DisbursementModelInsert(t *testing.T) {
assert.Equal(t, "user1", actual.StatusHistory[0].UserID)
assert.Equal(t, VerificationTypeDateOfBirth, actual.VerificationField)
})
+
+ t.Run("๐ข successfully insert disbursement (empty:[VerificationField,ReceiverRegistrationMessageTemplate])", func(t *testing.T) {
+ defer DeleteAllDisbursementFixtures(t, ctx, dbConnectionPool)
+
+ d := disbursement
+ d.ReceiverRegistrationMessageTemplate = ""
+ d.VerificationField = ""
+
+ id, err := disbursementModel.Insert(ctx, &d)
+ require.NoError(t, err)
+ require.NotNil(t, id)
+
+ actual, err := disbursementModel.Get(ctx, dbConnectionPool, id)
+ require.NoError(t, err)
+
+ assert.Equal(t, "disbursement", actual.Name)
+ assert.Equal(t, DraftDisbursementStatus, actual.Status)
+ assert.Equal(t, asset, actual.Asset)
+ assert.Equal(t, wallet, actual.Wallet)
+ assert.Empty(t, actual.ReceiverRegistrationMessageTemplate)
+ assert.Equal(t, 1, len(actual.StatusHistory))
+ assert.Equal(t, DraftDisbursementStatus, actual.StatusHistory[0].Status)
+ assert.Equal(t, "user1", actual.StatusHistory[0].UserID)
+ assert.Empty(t, actual.VerificationField)
+ })
}
func Test_DisbursementModelCount(t *testing.T) {
diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go
index c17f3f2ba..69f79d0f5 100644
--- a/internal/serve/httphandler/disbursement_handler.go
+++ b/internal/serve/httphandler/disbursement_handler.go
@@ -61,11 +61,13 @@ func (d DisbursementHandler) validateRequest(req PostDisbursementRequest) *valid
"registration_contact_type",
fmt.Sprintf("registration_contact_type must be one of %v", data.AllRegistrationContactTypes()),
)
- v.Check(
- slices.Contains(data.GetAllVerificationTypes(), req.VerificationField),
- "verification_field",
- fmt.Sprintf("verification_field must be one of %v", data.GetAllVerificationTypes()),
- )
+ if !req.RegistrationContactType.IncludesWalletAddress {
+ v.Check(
+ slices.Contains(data.GetAllVerificationTypes(), req.VerificationField),
+ "verification_field",
+ fmt.Sprintf("verification_field must be one of %v", data.GetAllVerificationTypes()),
+ )
+ }
return v
}
diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go
index d8892602b..8641e89d0 100644
--- a/internal/serve/httphandler/disbursement_handler_test.go
+++ b/internal/serve/httphandler/disbursement_handler_test.go
@@ -38,11 +38,13 @@ import (
)
func Test_DisbursementHandler_validateRequest(t *testing.T) {
- testCases := []struct {
+ type TestCase struct {
name string
request PostDisbursementRequest
expectedErrors map[string]interface{}
- }{
+ }
+
+ testCases := []TestCase{
{
name: "๐ด all fields are empty",
request: PostDisbursementRequest{},
@@ -82,6 +84,32 @@ func Test_DisbursementHandler_validateRequest(t *testing.T) {
},
}
+ for _, rct := range data.AllRegistrationContactTypes() {
+ var name string
+ var expectedErrors map[string]interface{}
+ if !rct.IncludesWalletAddress {
+ name = fmt.Sprintf("๐ด[%s]registration_contact_type without wallet address REQUIRES verification_field", rct)
+ expectedErrors = map[string]interface{}{
+ "verification_field": fmt.Sprintf("verification_field must be one of %v", data.GetAllVerificationTypes()),
+ }
+ } else {
+ name = fmt.Sprintf("๐ข[%s]registration_contact_type with wallet address DOES NOT REQUIRE registration_contact_type", rct)
+ }
+ newTestCase := TestCase{
+ name: name,
+ request: PostDisbursementRequest{
+ Name: "disbursement 1",
+ AssetID: "61dbfa89-943a-413c-b862-a2177384d321",
+ WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
+ RegistrationContactType: rct,
+ VerificationField: "",
+ },
+ expectedErrors: expectedErrors,
+ }
+
+ testCases = append(testCases, newTestCase)
+ }
+
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handler := &DisbursementHandler{}
diff --git a/internal/utils/sql.go b/internal/utils/sql.go
new file mode 100644
index 000000000..c3f4b4e29
--- /dev/null
+++ b/internal/utils/sql.go
@@ -0,0 +1,10 @@
+package utils
+
+import "database/sql"
+
+func SQLNullString(s string) sql.NullString {
+ return sql.NullString{
+ String: s,
+ Valid: s != "",
+ }
+}
From 52e0a4d58029b6a866fa015ad4d59d15299bae2e Mon Sep 17 00:00:00 2001
From: Marwen Abid
Date: Tue, 5 Nov 2024 09:10:59 -0800
Subject: [PATCH 58/75] SDP-1374 Integrate Wallet Address in Processing
Disbursement Instructions (#453)
---
internal/data/disbursement_instructions.go | 77 +++++++++--
.../data/disbursement_instructions_test.go | 126 ++++++++++++++++--
internal/data/disbursements_test.go | 6 +-
internal/data/fixtures.go | 4 +
internal/data/fixtures_test.go | 10 +-
internal/data/receivers_wallet.go | 87 ++++++------
internal/data/receivers_wallet_test.go | 50 ++++++-
.../serve/httphandler/disbursement_handler.go | 85 ++++++++----
.../httphandler/disbursement_handler_test.go | 110 +++++++++++----
.../disbursement_instructions_validator.go | 84 ++++++------
...isbursement_instructions_validator_test.go | 92 ++++++++++++-
11 files changed, 552 insertions(+), 179 deletions(-)
diff --git a/internal/data/disbursement_instructions.go b/internal/data/disbursement_instructions.go
index 29cf37ec6..0c399ebec 100644
--- a/internal/data/disbursement_instructions.go
+++ b/internal/data/disbursement_instructions.go
@@ -4,7 +4,9 @@ import (
"context"
"errors"
"fmt"
+ "slices"
+ "github.com/stellar/go/support/log"
"golang.org/x/exp/maps"
"github.com/stellar/stellar-disbursement-platform-backend/db"
@@ -17,6 +19,7 @@ type DisbursementInstruction struct {
Amount string `csv:"amount"`
VerificationValue string `csv:"verification"`
ExternalPaymentId string `csv:"paymentID"`
+ WalletAddress string `csv:"walletAddress"`
}
func (di *DisbursementInstruction) Contact() (string, error) {
@@ -63,7 +66,6 @@ var (
type DisbursementInstructionsOpts struct {
UserID string
Instructions []*DisbursementInstruction
- ReceiverContactType ReceiverContactType
Disbursement *Disbursement
DisbursementUpdate *DisbursementUpdate
MaxNumberOfInstructions int
@@ -82,9 +84,10 @@ type DisbursementInstructionsOpts struct {
// | | | | | |--- If the verification value does not match and the verification is confirmed, return an error.
// | | | | | |--- If the verification value does not match and the verification is not confirmed, update the verification value.
// | | | | | |--- If the verification value matches, continue.
-// | | |--- Check if the receiver wallet exists.
+// | | |--- [!ReceiverContactType.IncludesWalletAddress] Check if the receiver wallet exists.
// | | | |--- If the receiver wallet does not exist, create one.
// | | | |--- If the receiver wallet exists and it's not REGISTERED, retry the invitation SMS.
+// | | |--- [ReceiverContactType.IncludesWalletAddress] Register the supplied wallet address
// | | |--- Delete all previously existing payments tied to this disbursement.
// | | |--- Create all payments passed in the instructions.
func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, opts DisbursementInstructionsOpts) error {
@@ -95,23 +98,30 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, opts Disb
// We need all the following logic to be executed in one transaction.
return db.RunInTransaction(ctx, di.dbConnectionPool, nil, func(dbTx db.DBTransaction) error {
// Step 1: Fetch all receivers by contact information (phone, email, etc.) and create missing ones
- receiversByIDMap, err := di.reconcileExistingReceiversWithInstructions(ctx, dbTx, opts.Instructions, opts.ReceiverContactType)
+ registrationContactType := opts.Disbursement.RegistrationContactType
+ receiversByIDMap, err := di.reconcileExistingReceiversWithInstructions(ctx, dbTx, opts.Instructions, registrationContactType.ReceiverContactType)
if err != nil {
return fmt.Errorf("processing receivers: %w", err)
}
- // Step 2: Fetch all receiver verifications and create missing ones.
- err = di.processReceiverVerifications(ctx, dbTx, receiversByIDMap, opts.Instructions, opts.Disbursement, opts.ReceiverContactType)
- if err != nil {
- return fmt.Errorf("processing receiver verifications: %w", err)
- }
-
- // Step 3: Fetch all receiver wallets and create missing ones
+ // Step 2: Fetch all receiver wallets and create missing ones
receiverIDToReceiverWalletIDMap, err := di.processReceiverWallets(ctx, dbTx, receiversByIDMap, opts.Disbursement)
if err != nil {
return fmt.Errorf("processing receiver wallets: %w", err)
}
+ // Step 3: Register supplied wallets or process receiver verifications based on the registration contact type
+ if registrationContactType.IncludesWalletAddress {
+ if err = di.registerSuppliedWallets(ctx, dbTx, opts.Instructions, receiversByIDMap, receiverIDToReceiverWalletIDMap); err != nil {
+ return fmt.Errorf("registering supplied wallets: %w", err)
+ }
+ } else {
+ err = di.processReceiverVerifications(ctx, dbTx, receiversByIDMap, opts.Instructions, opts.Disbursement, registrationContactType.ReceiverContactType)
+ if err != nil {
+ return fmt.Errorf("processing receiver verifications: %w", err)
+ }
+ }
+
// Step 4: Delete all pre-existing payments tied to this disbursement for each receiver in one call
if err = di.paymentModel.DeleteAllForDisbursement(ctx, dbTx, opts.Disbursement.ID); err != nil {
return fmt.Errorf("deleting payments: %w", err)
@@ -136,6 +146,53 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, opts Disb
})
}
+func (di DisbursementInstructionModel) registerSuppliedWallets(ctx context.Context, dbTx db.DBTransaction, instructions []*DisbursementInstruction, receiversByIDMap map[string]*Receiver, receiverIDToReceiverWalletIDMap map[string]string) error {
+ // Construct a map of receiverWalletID to receiverWallet
+ receiverWalletsByIDMap, err := di.getReceiverWalletsByIDMap(ctx, dbTx, maps.Values(receiverIDToReceiverWalletIDMap))
+ if err != nil {
+ return fmt.Errorf("building receiver wallets lookup map: %w", err)
+ }
+
+ // Mark receiver wallets as registered
+ for _, instruction := range instructions {
+ receiver := findReceiverByInstruction(receiversByIDMap, instruction)
+ if receiver == nil {
+ return fmt.Errorf("receiver not found for instruction with ID %s", instruction.ID)
+ }
+ receiverWalletID, exists := receiverIDToReceiverWalletIDMap[receiver.ID]
+ if !exists {
+ return fmt.Errorf("receiver wallet not found for receiver with ID %s", receiver.ID)
+ }
+ receiverWallet := receiverWalletsByIDMap[receiverWalletID]
+
+ if slices.Contains([]ReceiversWalletStatus{RegisteredReceiversWalletStatus, FlaggedReceiversWalletStatus}, receiverWallet.Status) {
+ log.Ctx(ctx).Infof("receiver wallet with ID %s is %s, skipping registration", receiverWallet.ID, receiverWallet.Status)
+ continue
+ }
+
+ receiverWalletUpdate := ReceiverWalletUpdate{
+ Status: RegisteredReceiversWalletStatus,
+ StellarAddress: instruction.WalletAddress,
+ }
+ if updateErr := di.receiverWalletModel.Update(ctx, receiverWalletID, receiverWalletUpdate, dbTx); updateErr != nil {
+ return fmt.Errorf("marking receiver wallet as registered: %w", updateErr)
+ }
+ }
+ return nil
+}
+
+func (di DisbursementInstructionModel) getReceiverWalletsByIDMap(ctx context.Context, dbTx db.DBTransaction, receiverWalletIDs []string) (map[string]ReceiverWallet, error) {
+ receiverWallets, err := di.receiverWalletModel.GetByIDs(ctx, dbTx, receiverWalletIDs...)
+ if err != nil {
+ return nil, fmt.Errorf("fetching receiver wallets: %w", err)
+ }
+ receiverWalletsByIDMap := make(map[string]ReceiverWallet, len(receiverWallets))
+ for _, receiverWallet := range receiverWallets {
+ receiverWalletsByIDMap[receiverWallet.ID] = receiverWallet
+ }
+ return receiverWalletsByIDMap, nil
+}
+
// reconcileExistingReceiversWithInstructions fetches all existing receivers by their contact information and creates missing ones.
func (di DisbursementInstructionModel) reconcileExistingReceiversWithInstructions(ctx context.Context, dbTx db.DBTransaction, instructions []*DisbursementInstruction, contactType ReceiverContactType) (map[string]*Receiver, error) {
// Step 1: Fetch existing receivers
diff --git a/internal/data/disbursement_instructions_test.go b/internal/data/disbursement_instructions_test.go
index 317f11ac3..80524a600 100644
--- a/internal/data/disbursement_instructions_test.go
+++ b/internal/data/disbursement_instructions_test.go
@@ -32,6 +32,13 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Wallet: wallet,
})
+ emailDisbursement := CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, &DisbursementModel{dbConnectionPool: dbConnectionPool}, Disbursement{
+ Name: "disbursement2",
+ Asset: asset,
+ Wallet: wallet,
+ RegistrationContactType: RegistrationContactTypeEmail,
+ })
+
di := NewDisbursementInstructionModel(dbConnectionPool)
smsInstruction1 := DisbursementInstruction{
@@ -91,6 +98,21 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
FileContent: CreateInstructionsFixture(t, smsInstructions),
}
+ knownWalletDisbursement := CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, &DisbursementModel{dbConnectionPool: dbConnectionPool}, Disbursement{
+ Name: "disbursement with provided receiver wallets",
+ Asset: asset,
+ Wallet: wallet,
+ RegistrationContactType: RegistrationContactTypePhoneAndWalletAddress,
+ })
+
+ knownWalletDisbursementUpdate := func(instructions []*DisbursementInstruction) *DisbursementUpdate {
+ return &DisbursementUpdate{
+ ID: knownWalletDisbursement.ID,
+ FileName: "instructions.csv",
+ FileContent: CreateInstructionsFixture(t, instructions),
+ }
+ }
+
cleanup := func() {
DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool)
DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool)
@@ -98,6 +120,96 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
DeleteAllReceiversFixtures(t, ctx, dbConnectionPool)
}
+ t.Run("failure - invalid wallet address for known wallet address instructions", func(t *testing.T) {
+ defer cleanup()
+
+ instructions := []*DisbursementInstruction{
+ {
+ WalletAddress: "GCVL44LFV3BFI627ABY3YRITFBRJVXUQVPLXQ3LISMI5UVKS5LHWTPT6",
+ Amount: "100.01",
+ ID: "1",
+ Phone: "+380-12-345-679",
+ },
+ }
+
+ err := di.ProcessAll(ctx, DisbursementInstructionsOpts{
+ UserID: "user-id",
+ Instructions: instructions,
+ Disbursement: knownWalletDisbursement,
+ DisbursementUpdate: knownWalletDisbursementUpdate(instructions),
+ MaxNumberOfInstructions: 10,
+ })
+ assert.ErrorContains(t, err, "validating receiver wallet update: invalid stellar address")
+ })
+
+ t.Run("success - known wallet address instructions", func(t *testing.T) {
+ defer cleanup()
+
+ instructions := []*DisbursementInstruction{
+ {
+ WalletAddress: "GCVL44LFV3BFI627ABY3YRITFBRJVXUQVPLXQ3LISMI5UVKS5LHWTPT7",
+ Amount: "100.01",
+ ID: "1",
+ Phone: "+380-12-345-671",
+ },
+ {
+ WalletAddress: "GC524YE6Z6ISMNLHWFYXQZRR5DTF2A75DYE5TE6G7UMZJ6KZRNVHPOQS",
+ Amount: "100.02",
+ ID: "2",
+ Phone: "+380-12-345-672",
+ },
+ }
+
+ update := knownWalletDisbursementUpdate(instructions)
+ err := di.ProcessAll(ctx, DisbursementInstructionsOpts{
+ UserID: "user-id",
+ Instructions: instructions,
+ Disbursement: knownWalletDisbursement,
+ DisbursementUpdate: update,
+ MaxNumberOfInstructions: 10,
+ })
+ require.NoError(t, err)
+
+ // Verify Receivers
+ receivers, err := di.receiverModel.GetByContacts(ctx, dbConnectionPool, instructions[0].Phone, instructions[1].Phone)
+ require.NoError(t, err)
+ assertEqualReceivers(t, []string{instructions[0].Phone, instructions[1].Phone}, []string{"1", "2"}, receivers)
+
+ // Verify Receiver Verifications
+ receiver1Verifications, err := di.receiverVerificationModel.GetAllByReceiverId(ctx, dbConnectionPool, receivers[0].ID)
+ require.NoError(t, err)
+ assert.Len(t, receiver1Verifications, 0)
+ receiver2Verifications, err := di.receiverVerificationModel.GetAllByReceiverId(ctx, dbConnectionPool, receivers[1].ID)
+ require.NoError(t, err)
+ assert.Len(t, receiver2Verifications, 0)
+
+ // Verify Receiver Wallets
+ receiverWallets, err := di.receiverWalletModel.GetWithReceiverIds(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID})
+ require.NoError(t, err)
+ assert.Len(t, receiverWallets, 2)
+ for _, receiverWallet := range receiverWallets {
+ assert.Equal(t, wallet.ID, receiverWallet.Wallet.ID)
+ assert.Contains(t, []string{instructions[0].WalletAddress, instructions[1].WalletAddress}, receiverWallet.StellarAddress)
+ assert.Equal(t, RegisteredReceiversWalletStatus, receiverWallet.Status)
+ }
+
+ // Verify Payments
+ actualPayments := GetPaymentsByDisbursementID(t, ctx, dbConnectionPool, knownWalletDisbursement.ID)
+ assert.Len(t, actualPayments, 2)
+ assert.Contains(t, actualPayments, instructions[0].Amount)
+ assert.Contains(t, actualPayments, instructions[1].Amount)
+
+ actualExternalPaymentIDs := GetExternalPaymentIDsByDisbursementID(t, ctx, dbConnectionPool, knownWalletDisbursement.ID)
+ assert.Len(t, actualExternalPaymentIDs, 0)
+
+ // Verify Disbursement
+ actualDisbursement, err := di.disbursementModel.Get(ctx, dbConnectionPool, knownWalletDisbursement.ID)
+ require.NoError(t, err)
+ assert.Equal(t, ReadyDisbursementStatus, actualDisbursement.Status)
+ assert.Equal(t, update.FileContent, actualDisbursement.FileContent)
+ assert.Equal(t, update.FileName, actualDisbursement.FileName)
+ })
+
t.Run("success - sms instructions", func(t *testing.T) {
defer cleanup()
@@ -106,7 +218,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: smsInstructions,
Disbursement: disbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeSMS,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.NoError(t, err)
@@ -152,9 +263,8 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
err := di.ProcessAll(ctx, DisbursementInstructionsOpts{
UserID: "user-id",
Instructions: emailInstructions,
- Disbursement: disbursement,
+ Disbursement: emailDisbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeEmail,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.NoError(t, err)
@@ -176,9 +286,8 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
err := di.ProcessAll(ctx, DisbursementInstructionsOpts{
UserID: "user-id",
Instructions: smsInstructions,
- Disbursement: disbursement,
+ Disbursement: emailDisbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeEmail,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.ErrorContains(t, err, "has no contact information for contact type EMAIL")
@@ -202,7 +311,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: emailAndSMSInstructions,
Disbursement: disbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeEmail,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
errorMsg := "processing receivers: resolving contact information for instruction with ID %s: phone and email are both provided"
@@ -218,7 +326,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: smsInstructions,
Disbursement: disbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeSMS,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.NoError(t, err)
@@ -229,7 +336,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: smsInstructions,
Disbursement: disbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeSMS,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.NoError(t, err)
@@ -298,7 +404,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: newInstructions,
Disbursement: readyDisbursement,
DisbursementUpdate: readyDisbursementUpdate,
- ReceiverContactType: ReceiverContactTypeSMS,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.NoError(t, err)
@@ -342,7 +447,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: newInstructions,
Disbursement: readyDisbursement,
DisbursementUpdate: readyDisbursementUpdate,
- ReceiverContactType: ReceiverContactTypeSMS,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.NoError(t, err)
@@ -383,7 +487,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: smsInstructions,
Disbursement: disbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeSMS,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.NoError(t, err)
@@ -406,7 +509,6 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) {
Instructions: smsInstructions,
Disbursement: disbursement,
DisbursementUpdate: disbursementUpdate,
- ReceiverContactType: ReceiverContactTypeSMS,
MaxNumberOfInstructions: MaxInstructionsPerDisbursement,
})
require.Error(t, err)
diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go
index d7877666d..7d81fb363 100644
--- a/internal/data/disbursements_test.go
+++ b/internal/data/disbursements_test.go
@@ -459,9 +459,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", ""},
+ {Phone: "1234567890", ID: "1", Amount: "123.12", VerificationValue: "1995-02-20"},
+ {Phone: "0987654321", ID: "2", Amount: "321", VerificationValue: "1974-07-19"},
+ {Phone: "0987654321", ID: "3", Amount: "321", VerificationValue: "1974-07-19"},
})
t.Run("update instructions", func(t *testing.T) {
diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go
index 3654b77bb..16a766515 100644
--- a/internal/data/fixtures.go
+++ b/internal/data/fixtures.go
@@ -594,6 +594,10 @@ func CreateDraftDisbursementFixture(t *testing.T, ctx context.Context, sqlExec d
insert.RegistrationContactType = RegistrationContactTypePhone
}
+ if utils.IsEmpty(insert.RegistrationContactType) {
+ insert.RegistrationContactType = RegistrationContactTypePhone
+ }
+
id, err := model.Insert(ctx, &insert)
require.NoError(t, err)
diff --git a/internal/data/fixtures_test.go b/internal/data/fixtures_test.go
index 1b41adaeb..57533bda3 100644
--- a/internal/data/fixtures_test.go
+++ b/internal/data/fixtures_test.go
@@ -90,8 +90,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", ""},
+ {Phone: "1234567890", ID: "1", Amount: "123.12", VerificationValue: "1995-02-20"},
+ {Phone: "0987654321", ID: "2", Amount: "321", VerificationValue: "1974-07-19"},
}
buf := CreateInstructionsFixture(t, instructions)
lines := strings.Split(string(buf), "\n")
@@ -117,9 +117,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", ""},
+ {Phone: "1234567890", ID: "1", Amount: "123.12", VerificationValue: "1995-02-20"},
+ {Phone: "0987654321", ID: "2", Amount: "321", VerificationValue: "1974-07-19"},
+ {Phone: "0987654321", ID: "3", Amount: "321", VerificationValue: "1974-07-19"},
}
t.Run("update instructions", func(t *testing.T) {
diff --git a/internal/data/receivers_wallet.go b/internal/data/receivers_wallet.go
index 2ca5e6734..db88b9ae5 100644
--- a/internal/data/receivers_wallet.go
+++ b/internal/data/receivers_wallet.go
@@ -206,6 +206,45 @@ func (rw *ReceiverWalletModel) GetWithReceiverIds(ctx context.Context, sqlExec d
return receiverWallets, nil
}
+const selectReceiverWalletQuery = `
+ SELECT
+ rw.id,
+ rw.receiver_id as "receiver.id",
+ rw.status,
+ COALESCE(rw.anchor_platform_transaction_id, '') as anchor_platform_transaction_id,
+ COALESCE(rw.stellar_address, '') as stellar_address,
+ COALESCE(rw.stellar_memo, '') as stellar_memo,
+ COALESCE(rw.stellar_memo_type, '') as stellar_memo_type,
+ COALESCE(rw.otp, '') as otp,
+ rw.otp_created_at,
+ rw.otp_confirmed_at,
+ COALESCE(rw.otp_confirmed_with, '') as otp_confirmed_with,
+ w.id as "wallet.id",
+ w.name as "wallet.name",
+ w.sep_10_client_domain as "wallet.sep_10_client_domain",
+ w.homepage as "wallet.homepage"
+ FROM
+ receiver_wallets rw
+ JOIN
+ wallets w ON rw.wallet_id = w.id
+ `
+
+// GetByIDs returns a receiver wallet by IDs
+func (rw *ReceiverWalletModel) GetByIDs(ctx context.Context, sqlExec db.SQLExecuter, ids ...string) ([]ReceiverWallet, error) {
+ if len(ids) == 0 {
+ return nil, fmt.Errorf("no receiver wallet IDs provided")
+ }
+
+ query := fmt.Sprintf("%s WHERE rw.id = ANY($1)", selectReceiverWalletQuery)
+
+ receiverWallets := make([]ReceiverWallet, len(ids))
+ err := sqlExec.SelectContext(ctx, &receiverWallets, query, pq.Array(ids))
+ if err != nil {
+ return nil, fmt.Errorf("querying receiver wallet: %w", err)
+ }
+ return receiverWallets, nil
+}
+
// GetByReceiverIDsAndWalletID returns a list of receiver wallets by receiver IDs and wallet ID.
func (rw *ReceiverWalletModel) GetByReceiverIDsAndWalletID(ctx context.Context, sqlExec db.SQLExecuter, receiverIds []string, walletId string) ([]*ReceiverWallet, error) {
receiverWallets := []*ReceiverWallet{}
@@ -350,33 +389,9 @@ func (rw *ReceiverWalletModel) Insert(ctx context.Context, sqlExec db.SQLExecute
// GetByReceiverIDAndWalletDomain returns a receiver wallet that match the receiver ID and wallet domain.
func (rw *ReceiverWalletModel) GetByReceiverIDAndWalletDomain(ctx context.Context, receiverId string, walletDomain string, sqlExec db.SQLExecuter) (*ReceiverWallet, error) {
- var receiverWallet ReceiverWallet
- query := `
- SELECT
- rw.id,
- rw.receiver_id as "receiver.id",
- rw.status,
- COALESCE(rw.anchor_platform_transaction_id, '') as anchor_platform_transaction_id,
- COALESCE(rw.stellar_address, '') as stellar_address,
- COALESCE(rw.stellar_memo, '') as stellar_memo,
- COALESCE(rw.stellar_memo_type, '') as stellar_memo_type,
- COALESCE(rw.otp, '') as otp,
- rw.otp_created_at,
- rw.otp_confirmed_at,
- COALESCE(rw.otp_confirmed_with, '') as otp_confirmed_with,
- w.id as "wallet.id",
- w.name as "wallet.name",
- w.sep_10_client_domain as "wallet.sep_10_client_domain"
- FROM
- receiver_wallets rw
- JOIN
- wallets w ON rw.wallet_id = w.id
- WHERE
- rw.receiver_id = $1
- AND
- w.sep_10_client_domain = $2
- `
+ query := fmt.Sprintf("%s %s", selectReceiverWalletQuery, "WHERE rw.receiver_id = $1 AND w.sep_10_client_domain = $2")
+ var receiverWallet ReceiverWallet
err := sqlExec.GetContext(ctx, &receiverWallet, query, receiverId, walletDomain)
if err != nil {
return nil, fmt.Errorf("error querying receiver wallet: %w", err)
@@ -448,25 +463,7 @@ func (rw *ReceiverWalletModel) UpdateStatusByDisbursementID(ctx context.Context,
func (rw *ReceiverWalletModel) GetByStellarAccountAndMemo(ctx context.Context, stellarAccount, stellarMemo, clientDomain string) (*ReceiverWallet, error) {
// build query
var receiverWallets ReceiverWallet
- query := `
- SELECT
- rw.id,
- rw.receiver_id as "receiver.id",
- rw.status,
- COALESCE(rw.anchor_platform_transaction_id, '') as anchor_platform_transaction_id,
- COALESCE(rw.stellar_address, '') as stellar_address,
- COALESCE(rw.stellar_memo, '') as stellar_memo,
- COALESCE(rw.stellar_memo_type, '') as stellar_memo_type,
- COALESCE(rw.otp, '') as otp,
- rw.otp_created_at,
- COALESCE(rw.otp_confirmed_with, '') as otp_confirmed_with,
- w.id as "wallet.id",
- w.name as "wallet.name",
- w.homepage as "wallet.homepage"
- FROM receiver_wallets rw
- JOIN wallets w ON rw.wallet_id = w.id
- WHERE rw.stellar_address = ?
- `
+ query := fmt.Sprintf("%s %s", selectReceiverWalletQuery, "WHERE rw.stellar_address = ?")
// append memo to query if it is not empty
args := []interface{}{stellarAccount}
diff --git a/internal/data/receivers_wallet_test.go b/internal/data/receivers_wallet_test.go
index f2a670009..7ec43cc5d 100644
--- a/internal/data/receivers_wallet_test.go
+++ b/internal/data/receivers_wallet_test.go
@@ -502,6 +502,7 @@ func Test_GetByReceiverIDAndWalletDomain(t *testing.T) {
Wallet: Wallet{
ID: wallet.ID,
Name: wallet.Name,
+ Homepage: wallet.Homepage,
SEP10ClientDomain: wallet.SEP10ClientDomain,
},
Status: receiverWallet.Status,
@@ -1257,9 +1258,10 @@ func Test_GetByStellarAccountAndMemo(t *testing.T) {
ID: receiverWallet.ID,
Receiver: Receiver{ID: receiver.ID},
Wallet: Wallet{
- ID: wallet.ID,
- Name: wallet.Name,
- Homepage: wallet.Homepage,
+ ID: wallet.ID,
+ Name: wallet.Name,
+ Homepage: wallet.Homepage,
+ SEP10ClientDomain: wallet.SEP10ClientDomain,
},
Status: receiverWallet.Status,
OTP: "123456",
@@ -1285,9 +1287,10 @@ func Test_GetByStellarAccountAndMemo(t *testing.T) {
ID: receiverWallet.ID,
Receiver: Receiver{ID: receiver.ID},
Wallet: Wallet{
- ID: wallet.ID,
- Name: wallet.Name,
- Homepage: wallet.Homepage,
+ ID: wallet.ID,
+ Name: wallet.Name,
+ Homepage: wallet.Homepage,
+ SEP10ClientDomain: wallet.SEP10ClientDomain,
},
Status: receiverWallet.Status,
OTP: "123456",
@@ -1650,3 +1653,38 @@ func Test_ReceiverWalletModel_Update(t *testing.T) {
assert.Equal(t, RegisteredReceiversWalletStatus, statusHistory[0].Status)
})
}
+
+func Test_ReceiverWalletModel_GetByIDs(t *testing.T) {
+ dbt := dbtest.Open(t)
+ defer dbt.Close()
+
+ dbConnectionPool, outerErr := db.OpenDBConnectionPool(dbt.DSN)
+ require.NoError(t, outerErr)
+ defer dbConnectionPool.Close()
+
+ ctx := context.Background()
+ receiverWalletModel := ReceiverWalletModel{dbConnectionPool: dbConnectionPool}
+
+ t.Run("returns error when no receiver wallet IDs are provided", func(t *testing.T) {
+ rws, err := receiverWalletModel.GetByIDs(ctx, dbConnectionPool)
+ assert.EqualError(t, err, "no receiver wallet IDs provided")
+ assert.Empty(t, rws)
+ })
+
+ t.Run("returns no receiver wallets when IDs are invalid", func(t *testing.T) {
+ rws, err := receiverWalletModel.GetByIDs(ctx, dbConnectionPool, "invalid_id")
+ require.NoError(t, err)
+ assert.Empty(t, rws)
+ })
+
+ t.Run("๐successfully return receiver wallet when it exists", func(t *testing.T) {
+ receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{})
+ wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://")
+ receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, DraftReceiversWalletStatus)
+
+ rws, err := receiverWalletModel.GetByIDs(ctx, dbConnectionPool, receiverWallet.ID)
+ require.NoError(t, err)
+ require.Len(t, rws, 1)
+ assert.Equal(t, receiverWallet.ID, rws[0].ID)
+ })
+}
diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go
index 69f79d0f5..81286e53b 100644
--- a/internal/serve/httphandler/disbursement_handler.go
+++ b/internal/serve/httphandler/disbursement_handler.go
@@ -12,7 +12,6 @@ import (
"net/http"
"path/filepath"
"slices"
- "strings"
"time"
"github.com/go-chi/chi/v5"
@@ -225,14 +224,15 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter,
return
}
- contactType, err := resolveReceiverContactType(bytes.NewReader(buf.Bytes()))
- if err != nil {
- errMsg := fmt.Sprintf("could not determine contact information type: %s", err)
+ if err = validateCSVHeaders(bytes.NewReader(buf.Bytes()), disbursement.RegistrationContactType); err != nil {
+ errMsg := fmt.Sprintf("CSV columns are not valid for registration contact type %s: %s",
+ disbursement.RegistrationContactType,
+ err)
httperror.BadRequest(errMsg, err, nil).Render(w)
return
}
- instructions, v := parseInstructionsFromCSV(ctx, bytes.NewReader(buf.Bytes()), disbursement.VerificationField)
+ instructions, v := parseInstructionsFromCSV(ctx, bytes.NewReader(buf.Bytes()), disbursement.RegistrationContactType, disbursement.VerificationField)
if v != nil && v.HasErrors() {
httperror.BadRequest("could not parse csv file", err, v.Errors).Render(w)
return
@@ -260,7 +260,6 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter,
if err = d.Models.DisbursementInstructions.ProcessAll(ctx, data.DisbursementInstructionsOpts{
UserID: user.ID,
Instructions: instructions,
- ReceiverContactType: contactType,
Disbursement: disbursement,
DisbursementUpdate: disbursementUpdate,
MaxNumberOfInstructions: data.MaxInstructionsPerDisbursement,
@@ -487,8 +486,8 @@ func (d DisbursementHandler) GetDisbursementInstructions(w http.ResponseWriter,
}
// parseInstructionsFromCSV parses the CSV file and returns a list of DisbursementInstructions
-func parseInstructionsFromCSV(ctx context.Context, reader io.Reader, verificationField data.VerificationType) ([]*data.DisbursementInstruction, *validators.DisbursementInstructionsValidator) {
- validator := validators.NewDisbursementInstructionsValidator(verificationField)
+func parseInstructionsFromCSV(ctx context.Context, reader io.Reader, contactType data.RegistrationContactType, verificationField data.VerificationType) ([]*data.DisbursementInstruction, *validators.DisbursementInstructionsValidator) {
+ validator := validators.NewDisbursementInstructionsValidator(contactType, verificationField)
instructions := []*data.DisbursementInstruction{}
if err := gocsv.Unmarshal(reader, &instructions); err != nil {
@@ -514,33 +513,63 @@ func parseInstructionsFromCSV(ctx context.Context, reader io.Reader, verificatio
return sanitizedInstructions, nil
}
-// resolveReceiverContactType determines the type of contact information in the CSV file
-func resolveReceiverContactType(file io.Reader) (data.ReceiverContactType, error) {
+// validateCSVHeaders validates the headers of the CSV file to make sure we're passing the correct columns.
+func validateCSVHeaders(file io.Reader, registrationContactType data.RegistrationContactType) error {
headers, err := csv.NewReader(file).Read()
if err != nil {
- return "", fmt.Errorf("reading csv headers: %w", err)
+ return fmt.Errorf("reading csv headers: %w", err)
+ }
+
+ hasHeaders := map[string]bool{
+ "phone": false,
+ "email": false,
+ "walletAddress": false,
+ "verification": false,
}
- var hasPhone, hasEmail bool
+ // Populate header presence map
for _, header := range headers {
- switch strings.ToLower(strings.TrimSpace(header)) {
- case "phone":
- hasPhone = true
- case "email":
- hasEmail = true
+ if _, exists := hasHeaders[header]; exists {
+ hasHeaders[header] = true
}
}
- switch {
- case !hasPhone && !hasEmail:
- return "", fmt.Errorf("csv file must contain at least one of the following columns [phone, email]")
- case hasPhone && hasEmail:
- return "", fmt.Errorf("csv file must contain either a phone or email column, not both")
- case hasPhone:
- return data.ReceiverContactTypeSMS, nil
- case hasEmail:
- return data.ReceiverContactTypeEmail, nil
- default:
- return "", fmt.Errorf("csv file must contain either a phone or email column")
+ // establish the header rules. Each registration contact type has its own rules.
+ type headerRules struct {
+ required []string
+ disallowed []string
+ }
+
+ rules := map[data.RegistrationContactType]headerRules{
+ data.RegistrationContactTypePhone: {
+ required: []string{"phone", "verification"},
+ disallowed: []string{"email", "walletAddress"},
+ },
+ data.RegistrationContactTypeEmail: {
+ required: []string{"email", "verification"},
+ disallowed: []string{"phone", "walletAddress"},
+ },
+ data.RegistrationContactTypeEmailAndWalletAddress: {
+ required: []string{"email", "walletAddress"},
+ disallowed: []string{"phone", "verification"},
+ },
+ data.RegistrationContactTypePhoneAndWalletAddress: {
+ required: []string{"phone", "walletAddress"},
+ disallowed: []string{"email", "verification"},
+ },
+ }
+
+ // Validate headers according to the rules
+ for _, req := range rules[registrationContactType].required {
+ if !hasHeaders[req] {
+ return fmt.Errorf("%s column is required", req)
+ }
+ }
+ for _, dis := range rules[registrationContactType].disallowed {
+ if hasHeaders[dis] {
+ return fmt.Errorf("%s column is not allowed for this registration contact type", dis)
+ }
}
+
+ return nil
}
diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go
index 8641e89d0..b94a43994 100644
--- a/internal/serve/httphandler/disbursement_handler_test.go
+++ b/internal/serve/httphandler/disbursement_handler_test.go
@@ -863,12 +863,26 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
asset := data.GetAssetFixture(t, ctx, dbConnectionPool, data.FixtureAssetUSDC)
// create disbursement
- draftDisbursement := data.CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, handler.Models.Disbursements, data.Disbursement{
+ phoneDraftDisbursement := data.CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, handler.Models.Disbursements, data.Disbursement{
Name: "disbursement1",
Asset: asset,
Wallet: wallet,
})
+ emailDraftDisbursement := data.CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, handler.Models.Disbursements, data.Disbursement{
+ Name: "disbursement with emails",
+ Asset: asset,
+ Wallet: wallet,
+ RegistrationContactType: data.RegistrationContactTypeEmail,
+ })
+
+ emailWalletDraftDisbursement := data.CreateDraftDisbursementFixture(t, ctx, dbConnectionPool, handler.Models.Disbursements, data.Disbursement{
+ Name: "disbursement with emails and wallets",
+ Asset: asset,
+ Wallet: wallet,
+ RegistrationContactType: data.RegistrationContactTypeEmailAndWalletAddress,
+ })
+
startedDisbursement := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, handler.Models.Disbursements, &data.Disbursement{
Name: "disbursement 1",
Status: data.StartedDisbursementStatus,
@@ -895,7 +909,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
}{
{
name: "valid input",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
{"phone", "id", "amount", "verification"},
{"+380445555555", "123456789", "100.5", "1990-01-01"},
@@ -905,7 +919,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
},
{
name: ".bat file fails",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
{"phone", "id", "amount", "verification"},
{"+380445555555", "123456789", "100.5", "1990-01-01"},
@@ -916,7 +930,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
},
{
name: ".sh file fails",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
{"phone", "id", "amount", "verification"},
{"+380445555555", "123456789", "100.5", "1990-01-01"},
@@ -927,7 +941,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
},
{
name: ".bash file fails",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
{"phone", "id", "amount", "verification"},
{"+380445555555", "123456789", "100.5", "1990-01-01"},
@@ -938,7 +952,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
},
{
name: ".csv file with transversal path ..\\.. fails",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
{"phone", "id", "amount", "verification"},
{"+380445555555", "123456789", "100.5", "1990-01-01"},
@@ -949,7 +963,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
},
{
name: "invalid date of birth",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
{"phone", "id", "amount", "verification"},
{"+380445555555", "123456789", "100.5", "1990/01/01"},
@@ -959,7 +973,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
},
{
name: "invalid phone number",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
{"phone", "id", "amount", "verification"},
{"380-12-345-678", "123456789", "100.5", "1990-01-01"},
@@ -975,7 +989,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
},
{
name: "invalid input",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
multipartFieldName: "instructions",
expectedStatus: http.StatusBadRequest,
expectedMessage: "could not parse file",
@@ -993,37 +1007,87 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) {
expectedMessage: "disbursement is not in draft or ready status",
},
{
- name: "error parsing contact type from header",
- disbursementID: draftDisbursement.ID,
+ name: "no instructions found in file",
+ disbursementID: phoneDraftDisbursement.ID,
+ csvRecords: [][]string{
+ {"phone", "id", "amount", "verification"},
+ },
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: "could not parse csv file",
+ },
+ {
+ name: "headers invalid - email column missing for email contact type",
+ disbursementID: emailDraftDisbursement.ID,
csvRecords: [][]string{
{"id", "amount", "verification"},
{"123456789", "100.5", "1990-01-01"},
},
- expectedStatus: http.StatusBadRequest,
- expectedMessage: "could not determine contact information type",
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: fmt.Sprintf(
+ "CSV columns are not valid for registration contact type %s: email column is required",
+ data.RegistrationContactTypeEmail),
},
{
- name: "no instructions found in file",
- disbursementID: draftDisbursement.ID,
+ name: "columns invalid - email column not allowed for phone contact type",
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: [][]string{
- {"phone", "id", "amount", "date-of-birth"},
+ {"phone", "email", "id", "amount", "verification"},
+ {"+380445555555", "foobar@test.com", "123456789", "100.5", "1990-01-01"},
},
- expectedStatus: http.StatusBadRequest,
- expectedMessage: "could not parse csv file",
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: fmt.Sprintf(
+ "CSV columns are not valid for registration contact type %s: email column is not allowed for this registration contact type",
+ data.RegistrationContactTypePhone),
},
{
- name: "instructions invalid - attempting to upload phone and email",
- disbursementID: draftDisbursement.ID,
+ name: "columns invalid - phone column not allowed for email contact type",
+ disbursementID: emailDraftDisbursement.ID,
csvRecords: [][]string{
- {"phone", "email", "id", "amount", "date-of-birth"},
+ {"phone", "email", "id", "amount", "verification"},
{"+380445555555", "foobar@test.com", "123456789", "100.5", "1990-01-01"},
},
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: fmt.Sprintf(
+ "CSV columns are not valid for registration contact type %s: phone column is not allowed for this registration contact type",
+ data.RegistrationContactTypeEmail),
+ },
+ {
+ name: "columns invalid - wallet column missing for email-wallet contact type",
+ disbursementID: emailWalletDraftDisbursement.ID,
+ csvRecords: [][]string{
+ {"email", "id", "amount"},
+ {"foobar@test.com", "123456789", "100.5"},
+ },
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: fmt.Sprintf(
+ "CSV columns are not valid for registration contact type %s: walletAddress column is required",
+ data.RegistrationContactTypeEmailAndWalletAddress),
+ },
+ {
+ name: "columns invalid - verification column not allowed for wallet contact type",
+ disbursementID: emailWalletDraftDisbursement.ID,
+ csvRecords: [][]string{
+ {"walletAddress", "email", "id", "amount", "verification"},
+ {"GB3SAK22KSTIFQAV5GCDNPW7RTQCWGFDKALBY5KJ3JRF2DLSED3E7PVH", "foobar@test.com", "123456789", "100.5", "1990-01-01"},
+ },
+ expectedStatus: http.StatusBadRequest,
+ expectedMessage: fmt.Sprintf(
+ "CSV columns are not valid for registration contact type %s: verification column is not allowed for this registration contact type",
+ data.RegistrationContactTypeEmailAndWalletAddress),
+ },
+ {
+ name: "instructions invalid - walletAddress is invalid",
+ disbursementID: emailWalletDraftDisbursement.ID,
+ csvRecords: [][]string{
+ {"walletAddress", "email", "id", "amount"},
+ {"GB3SAK22KSTIFQAV5GKALBY5KJ3JRF2DLSED3E7PVH", "foobar@test.com", "123456789", "100.5"},
+ },
expectedStatus: http.StatusBadRequest,
- expectedMessage: "csv file must contain either a phone or email column, not both",
+ expectedMessage: "invalid wallet address",
},
{
name: "max instructions exceeded",
- disbursementID: draftDisbursement.ID,
+ disbursementID: phoneDraftDisbursement.ID,
csvRecords: maxCSVRecords,
expectedStatus: http.StatusBadRequest,
expectedMessage: "number of instructions exceeds maximum of 10000",
diff --git a/internal/serve/validators/disbursement_instructions_validator.go b/internal/serve/validators/disbursement_instructions_validator.go
index 183fc8f54..0d296e519 100644
--- a/internal/serve/validators/disbursement_instructions_validator.go
+++ b/internal/serve/validators/disbursement_instructions_validator.go
@@ -4,64 +4,64 @@ import (
"fmt"
"strings"
+ "github.com/stellar/go/strkey"
+
"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
)
type DisbursementInstructionsValidator struct {
+ contactType data.RegistrationContactType
verificationField data.VerificationType
*Validator
}
-func NewDisbursementInstructionsValidator(verificationField data.VerificationType) *DisbursementInstructionsValidator {
+func NewDisbursementInstructionsValidator(contactType data.RegistrationContactType, verificationField data.VerificationType) *DisbursementInstructionsValidator {
return &DisbursementInstructionsValidator{
+ contactType: contactType,
verificationField: verificationField,
Validator: NewValidator(),
}
}
func (iv *DisbursementInstructionsValidator) ValidateInstruction(instruction *data.DisbursementInstruction, lineNumber int) {
- var phone, email string
- if instruction.Phone != "" {
- phone = strings.TrimSpace(instruction.Phone)
- }
- if instruction.Email != "" {
- email = strings.TrimSpace(instruction.Email)
- }
-
- id := strings.TrimSpace(instruction.ID)
- amount := strings.TrimSpace(instruction.Amount)
- verification := strings.TrimSpace(instruction.VerificationValue)
-
- // validate contact field provided
- iv.Check(phone != "" || email != "", fmt.Sprintf("line %d - contact", lineNumber), "phone or email must be provided")
-
- // validate phone field
- if phone != "" {
- iv.CheckError(utils.ValidatePhoneNumber(phone), fmt.Sprintf("line %d - phone", lineNumber), "invalid phone format. Correct format: +380445555555")
+ // 1. Validate required fields
+ iv.Check(instruction.ID != "", fmt.Sprintf("line %d - id", lineNumber), "id cannot be empty")
+ iv.CheckError(utils.ValidateAmount(instruction.Amount), fmt.Sprintf("line %d - amount", lineNumber), "invalid amount. Amount must be a positive number")
+
+ // 2. Validate Contact fields
+ switch iv.contactType.ReceiverContactType {
+ case data.ReceiverContactTypeEmail:
+ iv.Check(instruction.Email != "", fmt.Sprintf("line %d - email", lineNumber), "email cannot be empty")
+ if instruction.Email != "" {
+ iv.CheckError(utils.ValidateEmail(instruction.Email), fmt.Sprintf("line %d - email", lineNumber), "invalid email format")
+ }
+ case data.ReceiverContactTypeSMS:
+ iv.Check(instruction.Phone != "", fmt.Sprintf("line %d - phone", lineNumber), "phone cannot be empty")
+ if instruction.Phone != "" {
+ iv.CheckError(utils.ValidatePhoneNumber(instruction.Phone), fmt.Sprintf("line %d - phone", lineNumber), "invalid phone format. Correct format: +380445555555")
+ }
}
- // validate email field
- if email != "" {
- iv.CheckError(utils.ValidateEmail(email), fmt.Sprintf("line %d - email", lineNumber), "invalid email format")
- }
-
- // validate id field
- iv.Check(id != "", fmt.Sprintf("line %d - id", lineNumber), "id cannot be empty")
-
- // validate amount field
- iv.CheckError(utils.ValidateAmount(amount), fmt.Sprintf("line %d - amount", lineNumber), "invalid amount. Amount must be a positive number")
-
- // validate verification field
- switch iv.verificationField {
- case data.VerificationTypeDateOfBirth:
- iv.CheckError(utils.ValidateDateOfBirthVerification(verification), fmt.Sprintf("line %d - date of birth", lineNumber), "")
- case data.VerificationTypeYearMonth:
- iv.CheckError(utils.ValidateYearMonthVerification(verification), fmt.Sprintf("line %d - year/month", lineNumber), "")
- case data.VerificationTypePin:
- iv.CheckError(utils.ValidatePinVerification(verification), fmt.Sprintf("line %d - pin", lineNumber), "")
- case data.VerificationTypeNationalID:
- iv.CheckError(utils.ValidateNationalIDVerification(verification), fmt.Sprintf("line %d - national id", lineNumber), "")
+ // 3. Validate WalletAddress field
+ if iv.contactType.IncludesWalletAddress {
+ iv.Check(instruction.WalletAddress != "", fmt.Sprintf("line %d - wallet address", lineNumber), "wallet address cannot be empty")
+ if instruction.WalletAddress != "" {
+ iv.Check(strkey.IsValidEd25519PublicKey(instruction.WalletAddress), fmt.Sprintf("line %d - wallet address", lineNumber), "invalid wallet address. Must be a valid Stellar public key")
+ }
+ } else {
+ // 4. Validate verification field
+ verification := instruction.VerificationValue
+ switch iv.verificationField {
+ case data.VerificationTypeDateOfBirth:
+ iv.CheckError(utils.ValidateDateOfBirthVerification(verification), fmt.Sprintf("line %d - date of birth", lineNumber), "")
+ case data.VerificationTypeYearMonth:
+ iv.CheckError(utils.ValidateYearMonthVerification(verification), fmt.Sprintf("line %d - year/month", lineNumber), "")
+ case data.VerificationTypePin:
+ iv.CheckError(utils.ValidatePinVerification(verification), fmt.Sprintf("line %d - pin", lineNumber), "")
+ case data.VerificationTypeNationalID:
+ iv.CheckError(utils.ValidateNationalIDVerification(verification), fmt.Sprintf("line %d - national id", lineNumber), "")
+ }
}
}
@@ -75,6 +75,10 @@ func (iv *DisbursementInstructionsValidator) SanitizeInstruction(instruction *da
sanitizedInstruction.Email = strings.ToLower(strings.TrimSpace(instruction.Email))
}
+ if instruction.WalletAddress != "" {
+ sanitizedInstruction.WalletAddress = strings.ToUpper(strings.TrimSpace(instruction.WalletAddress))
+ }
+
if instruction.ExternalPaymentId != "" {
sanitizedInstruction.ExternalPaymentId = strings.TrimSpace(instruction.ExternalPaymentId)
}
diff --git a/internal/serve/validators/disbursement_instructions_validator_test.go b/internal/serve/validators/disbursement_instructions_validator_test.go
index cdded4201..f7c18abad 100644
--- a/internal/serve/validators/disbursement_instructions_validator_test.go
+++ b/internal/serve/validators/disbursement_instructions_validator_test.go
@@ -13,35 +13,53 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
name string
instruction *data.DisbursementInstruction
lineNumber int
+ contactType data.RegistrationContactType
verificationField data.VerificationType
hasErrors bool
expectedErrors map[string]interface{}
}{
{
- name: "error if phone number and email are empty",
+ name: "error if phone number is empty for Phone contact type",
instruction: &data.DisbursementInstruction{
ID: "123456789",
Amount: "100.5",
VerificationValue: "1990-01-01",
},
lineNumber: 2,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
- "line 2 - contact": "phone or email must be provided",
+ "line 2 - phone": "phone cannot be empty",
},
},
{
- name: "error with all fields empty (phone, id, amount, date of birth)",
+ name: "error if email is empty for Email contact type",
+ instruction: &data.DisbursementInstruction{
+ ID: "123456789",
+ Amount: "100.5",
+ VerificationValue: "1990-01-01",
+ },
+ lineNumber: 2,
+ contactType: data.RegistrationContactTypeEmail,
+ verificationField: data.VerificationTypeDateOfBirth,
+ hasErrors: true,
+ expectedErrors: map[string]interface{}{
+ "line 2 - email": "email cannot be empty",
+ },
+ },
+ {
+ name: "error with all fields empty (phone, id, amount, verification)",
instruction: &data.DisbursementInstruction{},
lineNumber: 2,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
+ "line 2 - phone": "phone cannot be empty",
"line 2 - amount": "invalid amount. Amount must be a positive number",
- "line 2 - date of birth": "date of birth cannot be empty",
"line 2 - id": "id cannot be empty",
- "line 2 - contact": "phone or email must be provided",
+ "line 2 - date of birth": "date of birth cannot be empty",
},
},
{
@@ -53,6 +71,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990-01-01",
},
lineNumber: 2,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -68,6 +87,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990-01-01",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -83,6 +103,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990-01-01",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypeEmail,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -98,6 +119,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990-01-01",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -113,6 +135,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990/01/01",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -128,6 +151,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "2090-01-01",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -143,6 +167,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990/01",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeYearMonth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -158,6 +183,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "2090-01",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeYearMonth,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -173,6 +199,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "123",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypePin,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -188,6 +215,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "123456789",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypePin,
hasErrors: true,
expectedErrors: map[string]interface{}{
@@ -203,12 +231,44 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "6UZMB56FWTKV4U0PJ21TBR6VOQVYSGIMZG2HW2S0L7EK5K83W78",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeNationalID,
hasErrors: true,
expectedErrors: map[string]interface{}{
"line 3 - national id": "invalid national id. Cannot have more than 50 characters in national id",
},
},
+ {
+ name: "error when WalletAddress is empty for WalletAddress contact type",
+ instruction: &data.DisbursementInstruction{
+ WalletAddress: "",
+ Phone: "+380445555555",
+ ID: "123456789",
+ Amount: "100.5",
+ },
+ lineNumber: 3,
+ contactType: data.RegistrationContactTypePhoneAndWalletAddress,
+ hasErrors: true,
+ expectedErrors: map[string]interface{}{
+ "line 3 - wallet address": "wallet address cannot be empty",
+ },
+ },
+ {
+ name: "error when WalletAddress is not valid for WalletAddress contact type",
+ instruction: &data.DisbursementInstruction{
+ WalletAddress: "invalidwalletaddress",
+ Phone: "+380445555555",
+ ID: "123456789",
+ Amount: "100.5",
+ },
+ lineNumber: 3,
+ contactType: data.RegistrationContactTypePhoneAndWalletAddress,
+ hasErrors: true,
+ expectedErrors: map[string]interface{}{
+ "line 3 - wallet address": "invalid wallet address. Must be a valid Stellar public key",
+ },
+ },
+
// VALID CASES
{
name: "๐ successfully validates instructions (DATE_OF_BIRTH)",
@@ -219,6 +279,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990-01-01",
},
lineNumber: 1,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeDateOfBirth,
hasErrors: false,
},
@@ -231,6 +292,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1990-01",
},
lineNumber: 1,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeYearMonth,
hasErrors: false,
},
@@ -243,6 +305,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "ABCD123",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypeNationalID,
hasErrors: false,
},
@@ -255,6 +318,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1234",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypePin,
hasErrors: false,
},
@@ -267,6 +331,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1234",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypeEmail,
verificationField: data.VerificationTypePin,
hasErrors: false,
},
@@ -279,14 +344,27 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing
VerificationValue: "1234",
},
lineNumber: 3,
+ contactType: data.RegistrationContactTypePhone,
verificationField: data.VerificationTypePin,
hasErrors: false,
},
+ {
+ name: "๐ successfully validates instructions (WalletAddress)",
+ instruction: &data.DisbursementInstruction{
+ WalletAddress: "GB3SAK22KSTIFQAV5GCDNPW7RTQCWGFDKALBY5KJ3JRF2DLSED3E7PVH",
+ Phone: "+380445555555",
+ ID: "123456789",
+ Amount: "100.5",
+ },
+ lineNumber: 3,
+ contactType: data.RegistrationContactTypePhoneAndWalletAddress,
+ hasErrors: false,
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- iv := NewDisbursementInstructionsValidator(tt.verificationField)
+ iv := NewDisbursementInstructionsValidator(tt.contactType, tt.verificationField)
iv.ValidateInstruction(tt.instruction, tt.lineNumber)
if tt.hasErrors {
@@ -358,7 +436,7 @@ func Test_DisbursementInstructionsValidator_SanitizeInstruction(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- iv := NewDisbursementInstructionsValidator(data.VerificationTypeDateOfBirth)
+ iv := NewDisbursementInstructionsValidator(data.RegistrationContactTypePhone, data.VerificationTypeDateOfBirth)
sanitizedInstruction := iv.SanitizeInstruction(tt.actual)
assert.Equal(t, tt.expectedInstruction, sanitizedInstruction)
From b9369b0c6c7f37346071587e0cf29d8d7a114da5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 Nov 2024 11:34:47 -0800
Subject: [PATCH 59/75] Bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1
in the go_modules group (#457)
* Bump github.com/golang-jwt/jwt/v4 in the go_modules group
Bumps the go_modules group with 1 update: [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt).
Updates `github.com/golang-jwt/jwt/v4` from 4.5.0 to 4.5.1
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1)
---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
dependency-type: direct:production
dependency-group: go_modules
...
Signed-off-by: dependabot[bot]
* Execute go.list
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marcelo Salloum
---
go.list | 2 +-
go.mod | 2 +-
go.sum | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/go.list b/go.list
index de5c081b0..79720cd63 100644
--- a/go.list
+++ b/go.list
@@ -87,7 +87,7 @@ github.com/godror/knownpb v0.1.1
github.com/gofiber/fiber/v2 v2.52.2 => github.com/gofiber/fiber/v2 v2.52.5
github.com/gogo/protobuf v1.3.2
github.com/golang-jwt/jwt v3.2.2+incompatible
-github.com/golang-jwt/jwt/v4 v4.5.0
+github.com/golang-jwt/jwt/v4 v4.5.1
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golang/mock v1.6.0
diff --git a/go.mod b/go.mod
index e85f18791..71f1d42e7 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/httprate v0.14.1
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d
- github.com/golang-jwt/jwt/v4 v4.5.0
+ github.com/golang-jwt/jwt/v4 v4.5.1
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.4.1
github.com/jmoiron/sqlx v1.4.0
diff --git a/go.sum b/go.sum
index 6cc0fa705..b40be8475 100644
--- a/go.sum
+++ b/go.sum
@@ -57,8 +57,8 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d h1:KbPOUXFUDJxwZ04vbmDOc3yuruGvVO+LOa7cVER3yWw=
github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
-github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
-github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
+github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
From 831ae51fa4a8dec984e48d8dc51ccf461e770194 Mon Sep 17 00:00:00 2001
From: Marcelo Salloum dos Santos
Date: Wed, 6 Nov 2024 12:17:23 -0800
Subject: [PATCH 60/75] [SDP-1364] Prevent user from setting a custom invite
template with JS or HTML (#459)
### What
Prevent user from setting a custom invite template with JS or HTML
### Why
Address https://stellarorg.atlassian.net/browse/SDP-1364
---
.../serve/httphandler/disbursement_handler.go | 1 +
.../httphandler/disbursement_handler_test.go | 39 +++++++++++++++
internal/serve/httphandler/profile_handler.go | 30 +++++-------
.../serve/httphandler/profile_handler_test.go | 48 +++++++++++++++++++
internal/utils/validation.go | 17 +++++++
internal/utils/validation_test.go | 25 ++++++++++
6 files changed, 143 insertions(+), 17 deletions(-)
diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go
index 81286e53b..f7cd2cef3 100644
--- a/internal/serve/httphandler/disbursement_handler.go
+++ b/internal/serve/httphandler/disbursement_handler.go
@@ -60,6 +60,7 @@ func (d DisbursementHandler) validateRequest(req PostDisbursementRequest) *valid
"registration_contact_type",
fmt.Sprintf("registration_contact_type must be one of %v", data.AllRegistrationContactTypes()),
)
+ v.CheckError(utils.ValidateNoHTMLNorJSNorCSS(req.ReceiverRegistrationMessageTemplate), "receiver_registration_message_template", "receiver_registration_message_template cannot contain HTML, JS or CSS")
if !req.RegistrationContactType.IncludesWalletAddress {
v.Check(
slices.Contains(data.GetAllVerificationTypes(), req.VerificationField),
diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go
index b94a43994..bc4533bc2 100644
--- a/internal/serve/httphandler/disbursement_handler_test.go
+++ b/internal/serve/httphandler/disbursement_handler_test.go
@@ -72,6 +72,34 @@ func Test_DisbursementHandler_validateRequest(t *testing.T) {
"verification_field": fmt.Sprintf("verification_field must be one of %v", data.GetAllVerificationTypes()),
},
},
+ {
+ name: "๐ด receiver_registration_message_template contains HTML",
+ request: PostDisbursementRequest{
+ Name: "disbursement 1",
+ AssetID: "61dbfa89-943a-413c-b862-a2177384d321",
+ WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
+ ReceiverRegistrationMessageTemplate: "Redeem money",
+ },
+ expectedErrors: map[string]interface{}{
+ "receiver_registration_message_template": "receiver_registration_message_template cannot contain HTML, JS or CSS",
+ },
+ },
+ {
+ name: "๐ด receiver_registration_message_template contains JS",
+ request: PostDisbursementRequest{
+ Name: "disbursement 1",
+ AssetID: "61dbfa89-943a-413c-b862-a2177384d321",
+ WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
+ ReceiverRegistrationMessageTemplate: "javascript:alert(localStorage.getItem('sdp_session'))",
+ },
+ expectedErrors: map[string]interface{}{
+ "receiver_registration_message_template": "receiver_registration_message_template cannot contain HTML, JS or CSS",
+ },
+ },
{
name: "๐ข all fields are valid",
request: PostDisbursementRequest{
@@ -82,6 +110,17 @@ func Test_DisbursementHandler_validateRequest(t *testing.T) {
VerificationField: data.VerificationTypeDateOfBirth,
},
},
+ {
+ name: "๐ข all fields are valid w/ receiver_registration_message_template",
+ request: PostDisbursementRequest{
+ Name: "disbursement 1",
+ AssetID: "61dbfa89-943a-413c-b862-a2177384d321",
+ WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b",
+ RegistrationContactType: data.RegistrationContactTypePhone,
+ VerificationField: data.VerificationTypeDateOfBirth,
+ ReceiverRegistrationMessageTemplate: "My custom invitation message",
+ },
+ },
}
for _, rct := range data.AllRegistrationContactTypes() {
diff --git a/internal/serve/httphandler/profile_handler.go b/internal/serve/httphandler/profile_handler.go
index 07b334333..81ad1cbcb 100644
--- a/internal/serve/httphandler/profile_handler.go
+++ b/internal/serve/httphandler/profile_handler.go
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"image"
- "sort"
// Don't remove the `image/jpeg` and `image/png` packages import unless
// the `image` package is no longer necessary.
@@ -19,6 +18,7 @@ import (
"io/fs"
"net/http"
"net/url"
+ "sort"
"strings"
"github.com/stellar/go/support/http/httpdecode"
@@ -62,14 +62,7 @@ type PatchOrganizationProfileRequest struct {
}
func (r *PatchOrganizationProfileRequest) AreAllFieldsEmpty() bool {
- return r.OrganizationName == "" &&
- r.TimezoneUTCOffset == "" &&
- r.IsApprovalRequired == nil &&
- r.ReceiverRegistrationMessageTemplate == nil &&
- r.OTPMessageTemplate == nil &&
- r.ReceiverInvitationResendInterval == nil &&
- r.PaymentCancellationPeriodDays == nil &&
- r.PrivacyPolicyLink == nil
+ return r == nil || utils.IsEmpty(*r)
}
type PatchUserProfileRequest struct {
@@ -106,7 +99,7 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
// limiting the amount of memory allocated in the server to handle the request
if err := req.ParseMultipartForm(h.MaxMemoryAllocation); err != nil {
- err = fmt.Errorf("error parsing multipart form: %w", err)
+ err = fmt.Errorf("parsing multipart form: %w", err)
log.Ctx(ctx).Error(err)
httperror.BadRequest("could not parse multipart form data", err, map[string]interface{}{
"details": "request too large. Max size 2MB.",
@@ -116,7 +109,7 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
multipartFile, _, err := req.FormFile("logo")
if err != nil && !errors.Is(err, http.ErrMissingFile) {
- err = fmt.Errorf("error parsing logo file: %w", err)
+ err = fmt.Errorf("parsing logo file: %w", err)
log.Ctx(ctx).Error(err)
httperror.BadRequest("could not parse request logo", err, nil).Render(rw)
return
@@ -146,7 +139,7 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
var reqBody PatchOrganizationProfileRequest
d := req.FormValue("data")
if err = json.Unmarshal([]byte(d), &reqBody); err != nil {
- err = fmt.Errorf("error decoding data: %w", err)
+ err = fmt.Errorf("decoding data: %w", err)
log.Ctx(ctx).Error(err)
httperror.BadRequest("", err, nil).Render(rw)
return
@@ -160,17 +153,20 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
return
}
+ validator := validators.NewValidator()
if reqBody.PrivacyPolicyLink != nil && *reqBody.PrivacyPolicyLink != "" {
schemes := []string{"https"}
if !h.IsPubnet() {
schemes = append(schemes, "http")
}
- validator := validators.NewValidator()
validator.CheckError(utils.ValidateURLScheme(*reqBody.PrivacyPolicyLink, schemes...), "privacy_policy_link", "")
- if validator.HasErrors() {
- httperror.BadRequest("", nil, validator.Errors).Render(rw)
- return
- }
+ }
+ if reqBody.ReceiverRegistrationMessageTemplate != nil {
+ validator.CheckError(utils.ValidateNoHTMLNorJSNorCSS(*reqBody.ReceiverRegistrationMessageTemplate), "receiver_registration_message_template", "receiver_registration_message_template cannot contain HTML, JS or CSS")
+ }
+ if validator.HasErrors() {
+ httperror.BadRequest("", nil, validator.Errors).Render(rw)
+ return
}
organizationUpdate := data.OrganizationUpdate{
diff --git a/internal/serve/httphandler/profile_handler_test.go b/internal/serve/httphandler/profile_handler_test.go
index 09877723f..d91037a6b 100644
--- a/internal/serve/httphandler/profile_handler_test.go
+++ b/internal/serve/httphandler/profile_handler_test.go
@@ -300,6 +300,54 @@ func Test_ProfileHandler_PatchOrganizationProfile_Failures(t *testing.T) {
}
}`,
},
+ {
+ name: "returns BadRequest when receiver_registration_message_template contains HTML",
+ token: "token",
+ mockAuthManagerFn: func(authManagerMock *auth.AuthManagerMock) {
+ authManagerMock.
+ On("GetUser", mock.Anything, "token").
+ Return(user, nil).
+ Once()
+ },
+ getRequestFn: func(t *testing.T, ctx context.Context) *http.Request {
+ reqBody := `{
+ "receiver_registration_message_template": "Redeem money"
+ }`
+ return createOrganizationProfileMultipartRequest(t, ctx, url, "", "", reqBody, new(bytes.Buffer))
+ },
+ networkType: utils.PubnetNetworkType,
+ wantStatusCode: http.StatusBadRequest,
+ wantRespBody: `{
+ "error": "The request was invalid in some way.",
+ "extras": {
+ "receiver_registration_message_template": "receiver_registration_message_template cannot contain HTML, JS or CSS"
+ }
+ }`,
+ },
+ {
+ name: "returns BadRequest when receiver_registration_message_template contains JS",
+ token: "token",
+ mockAuthManagerFn: func(authManagerMock *auth.AuthManagerMock) {
+ authManagerMock.
+ On("GetUser", mock.Anything, "token").
+ Return(user, nil).
+ Once()
+ },
+ getRequestFn: func(t *testing.T, ctx context.Context) *http.Request {
+ reqBody := `{
+ "receiver_registration_message_template": "javascript:alert(localStorage.getItem('sdp_session'))"
+ }`
+ return createOrganizationProfileMultipartRequest(t, ctx, url, "", "", reqBody, new(bytes.Buffer))
+ },
+ networkType: utils.PubnetNetworkType,
+ wantStatusCode: http.StatusBadRequest,
+ wantRespBody: `{
+ "error": "The request was invalid in some way.",
+ "extras": {
+ "receiver_registration_message_template": "receiver_registration_message_template cannot contain HTML, JS or CSS"
+ }
+ }`,
+ },
}
for _, tc := range testCases {
diff --git a/internal/utils/validation.go b/internal/utils/validation.go
index b7ef60d0c..96a335d37 100644
--- a/internal/utils/validation.go
+++ b/internal/utils/validation.go
@@ -198,3 +198,20 @@ func ValidateURLScheme(link string, scheme ...string) error {
return nil
}
+
+// ValidateNoHTMLNorJSNorCSS detects HTML,