From 3307f478ed02cf0ec3b6089b2c3f3689b55cad98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 28 Jul 2024 02:59:17 +0000 Subject: [PATCH 01/75] Bump the all-docker group with 2 updates Bumps the all-docker group with 2 updates: golang and ubuntu. Updates `golang` from 1.22.1-bullseye to 1.22.5-bullseye Updates `ubuntu` from 22.04 to 24.04 --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-docker - dependency-name: ubuntu dependency-type: direct:production dependency-group: all-docker ... Signed-off-by: dependabot[bot] --- Dockerfile | 4 ++-- Dockerfile.development | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index c04e81fdb..52f64e8c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # To push: # make docker-push -FROM golang:1.22.1-bullseye AS build +FROM golang:1.22.5-bullseye AS build ARG GIT_COMMIT WORKDIR /src/stellar-disbursement-platform @@ -13,7 +13,7 @@ ADD . ./ RUN go build -o /bin/stellar-disbursement-platform -ldflags "-X main.GitCommit=$GIT_COMMIT" . -FROM ubuntu:22.04 +FROM ubuntu:24.04 RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates # ADD migrations/ /app/migrations/ diff --git a/Dockerfile.development b/Dockerfile.development index 8cab186a2..a6e04efac 100644 --- a/Dockerfile.development +++ b/Dockerfile.development @@ -1,5 +1,5 @@ # Stage 1: Build the Go application -FROM golang:1.22.1-bullseye AS build +FROM golang:1.22.5-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.22.1-bullseye AS development +FROM golang:1.22.5-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 25e364085b37aa0e3dbdbc7332bf229b6a2a2623 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:49:43 +0000 Subject: [PATCH 02/75] Bump the all-actions group across 1 directory with 5 updates (#360) --- .../workflows/anchor_platform_integration_check.yml | 2 +- .github/workflows/ci.yml | 10 +++++----- .github/workflows/docker_image_public_release.yml | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/anchor_platform_integration_check.yml b/.github/workflows/anchor_platform_integration_check.yml index 4de40c209..718683fe9 100644 --- a/.github/workflows/anchor_platform_integration_check.yml +++ b/.github/workflows/anchor_platform_integration_check.yml @@ -43,7 +43,7 @@ jobs: echo 'Anchor-platform is up and running.' - name: Install NodeJs - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: 14 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 898a498f9..04d04fd0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,14 +19,14 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 1.22.1 cache: true cache-dependency-path: go.sum - name: golangci-lint - uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # version v4.0.0 + uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # version v6.0.1 with: version: v1.56.2 # this is the golangci-lint version args: --timeout 5m0s @@ -76,7 +76,7 @@ jobs: uses: actions/checkout@v4 - name: Install NodeJs - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: 14 @@ -104,7 +104,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 1.22.1 cache: true @@ -143,7 +143,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 1.22.1 cache: true diff --git a/.github/workflows/docker_image_public_release.yml b/.github/workflows/docker_image_public_release.yml index e2e7ff2ee..fc91ec4cf 100644 --- a/.github/workflows/docker_image_public_release.yml +++ b/.github/workflows/docker_image_public_release.yml @@ -54,13 +54,13 @@ jobs: - uses: actions/checkout@v4 - name: Login to DockerHub - uses: docker/login-action@v2.2.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push to DockerHub (release prd) - uses: docker/build-push-action@v4.1.1 + uses: docker/build-push-action@v6.5.0 with: push: true build-args: | @@ -80,7 +80,7 @@ jobs: - uses: actions/checkout@v4 - name: Login to DockerHub - uses: docker/login-action@v2.2.0 + uses: docker/login-action@v3.3.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -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@v4.1.1 + uses: docker/build-push-action@v6.5.0 with: push: true build-args: | From 6b307cb9e2e6ea55fb15b01cb6d12efb2b4ecac5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 22:10:30 +0000 Subject: [PATCH 03/75] Bump golangci/golangci-lint-action (#380) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04d04fd0e..0f8cfecaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: cache-dependency-path: go.sum - name: golangci-lint - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # version v6.0.1 + uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # version v6.1.0 with: version: v1.56.2 # this is the golangci-lint version args: --timeout 5m0s From fe1785633aad6efd1ef2d05e26f80ccb15c8e267 Mon Sep 17 00:00:00 2001 From: Erica Liu Date: Wed, 31 Jul 2024 21:17:12 +0900 Subject: [PATCH 04/75] [Chore] Add tenant provisioning error details to response (#377) --- .../internal/httphandler/tenants_handler.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/stellar-multitenant/internal/httphandler/tenants_handler.go b/stellar-multitenant/internal/httphandler/tenants_handler.go index 9ccea2544..b16133b60 100644 --- a/stellar-multitenant/internal/httphandler/tenants_handler.go +++ b/stellar-multitenant/internal/httphandler/tenants_handler.go @@ -96,13 +96,17 @@ func (h TenantsHandler) Post(rw http.ResponseWriter, req *http.Request) { // sending the invitation message tntSDPUIBaseURL, err := h.generateTenantURL(reqBody.SDPUIBaseURL, h.SDPUIBaseURL, reqBody.Name) if err != nil { - httperror.InternalError(ctx, "Could not generate SDP UI URL", err, nil).Render(rw) + httperror.InternalError(ctx, "Could not generate SDP UI URL", err, map[string]interface{}{ + "error_details": err.Error(), + }).Render(rw) return } tntBaseURL, err := h.generateTenantURL(reqBody.BaseURL, h.BaseURL, reqBody.Name) if err != nil { - httperror.InternalError(ctx, "Could not generate URL", err, nil).Render(rw) + httperror.InternalError(ctx, "Could not generate URL", err, map[string]interface{}{ + "error_details": err.Error(), + }).Render(rw) return } @@ -122,7 +126,9 @@ func (h TenantsHandler) Post(rw http.ResponseWriter, req *http.Request) { httperror.BadRequest("Tenant name already exists", err, nil).Render(rw) return } - httperror.InternalError(ctx, "Could not provision a new tenant", err, nil).Render(rw) + httperror.InternalError(ctx, "Could not provision a new tenant", err, map[string]interface{}{ + "error_details": err.Error(), + }).Render(rw) return } From f6f7a50b397df749e5c23e94033c0323e3a30f56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:43:30 -0700 Subject: [PATCH 05/75] Bump the minor-and-patch group across 1 directory with 12 updates (#381) * Bump the minor-and-patch group across 1 directory with 12 updates Bumps the minor-and-patch group with 11 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) | `1.45.26` | `1.55.5` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.0.10` | `5.1.0` | | [github.com/go-chi/httprate](https://github.com/go-chi/httprate) | `0.8.0` | `0.12.0` | | [github.com/jmoiron/sqlx](https://github.com/jmoiron/sqlx) | `1.3.5` | `1.4.0` | | [github.com/nyaruka/phonenumbers](https://github.com/nyaruka/phonenumbers) | `1.1.8` | `1.4.0` | | [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `1.17.0` | `1.19.1` | | [github.com/rubenv/sql-migrate](https://github.com/rubenv/sql-migrate) | `1.5.2` | `1.7.0` | | [github.com/segmentio/kafka-go](https://github.com/segmentio/kafka-go) | `0.4.46` | `0.4.47` | | [github.com/spf13/cobra](https://github.com/spf13/cobra) | `1.7.0` | `1.8.1` | | [github.com/twilio/twilio-go](https://github.com/twilio/twilio-go) | `1.11.0` | `1.22.3` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.22.0` | `0.25.0` | Updates `github.com/aws/aws-sdk-go` from 1.45.26 to 1.55.5 - [Release notes](https://github.com/aws/aws-sdk-go/releases) - [Commits](https://github.com/aws/aws-sdk-go/compare/v1.45.26...v1.55.5) Updates `github.com/go-chi/chi/v5` from 5.0.10 to 5.1.0 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.0.10...v5.1.0) Updates `github.com/go-chi/httprate` from 0.8.0 to 0.12.0 - [Release notes](https://github.com/go-chi/httprate/releases) - [Commits](https://github.com/go-chi/httprate/compare/v0.8.0...v0.12.0) Updates `github.com/jmoiron/sqlx` from 1.3.5 to 1.4.0 - [Release notes](https://github.com/jmoiron/sqlx/releases) - [Commits](https://github.com/jmoiron/sqlx/compare/v1.3.5...v1.4.0) Updates `github.com/nyaruka/phonenumbers` from 1.1.8 to 1.4.0 - [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.1.8...v1.4.0) Updates `github.com/prometheus/client_golang` from 1.17.0 to 1.19.1 - [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.17.0...v1.19.1) Updates `github.com/rubenv/sql-migrate` from 1.5.2 to 1.7.0 - [Commits](https://github.com/rubenv/sql-migrate/compare/v1.5.2...v1.7.0) Updates `github.com/segmentio/kafka-go` from 0.4.46 to 0.4.47 - [Release notes](https://github.com/segmentio/kafka-go/releases) - [Commits](https://github.com/segmentio/kafka-go/compare/v0.4.46...v0.4.47) Updates `github.com/spf13/cobra` from 1.7.0 to 1.8.1 - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.7.0...v1.8.1) Updates `github.com/twilio/twilio-go` from 1.11.0 to 1.22.3 - [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.11.0...v1.22.3) Updates `golang.org/x/crypto` from 0.22.0 to 0.25.0 - [Commits](https://github.com/golang/crypto/compare/v0.22.0...v0.25.0) Updates `golang.org/x/exp` from 0.0.0-20231006140011-7918f672742d to 0.0.0-20240525044651-4c93da0ed11d - [Commits](https://github.com/golang/exp/commits) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/go-chi/httprate dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/jmoiron/sqlx dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/nyaruka/phonenumbers dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/rubenv/sql-migrate dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/segmentio/kafka-go dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: github.com/spf13/cobra dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: github.com/twilio/twilio-go dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: golang.org/x/exp 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 Co-authored-by: Marcelo Salloum dos Santos --- go.list | 58 +++++++++++++++---------------- go.mod | 34 +++++++++--------- go.sum | 106 ++++++++++++++++++++++---------------------------------- 3 files changed, 84 insertions(+), 114 deletions(-) diff --git a/go.list b/go.list index d24230bd5..5cceedf01 100644 --- a/go.list +++ b/go.list @@ -9,6 +9,7 @@ cloud.google.com/go/iam v1.1.8 cloud.google.com/go/longrunning v0.5.6 cloud.google.com/go/pubsub v1.37.0 cloud.google.com/go/storage v1.40.0 +filippo.io/edwards25519 v1.1.0 firebase.google.com/go v3.12.0+incompatible github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95 github.com/BurntSushi/toml v1.3.2 @@ -23,14 +24,14 @@ github.com/Microsoft/go-winio v0.6.1 github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 github.com/adjust/goautoneg v0.0.0-20150426214442-d788f35a0315 github.com/ajg/form v1.5.1 -github.com/alecthomas/kingpin/v2 v2.3.2 +github.com/alecthomas/kingpin/v2 v2.4.0 github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 github.com/andybalholm/brotli v1.1.0 github.com/armon/go-metrics v0.4.1 github.com/armon/go-radix v1.0.0 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.45.26 +github.com/aws/aws-sdk-go v1.55.5 github.com/aymerick/douceur v0.2.0 github.com/beevik/etree v1.1.0 github.com/beorn7/perks v1.0.1 @@ -38,14 +39,14 @@ github.com/bgentry/speakeasy v0.1.0 github.com/buger/goreplay v1.3.2 github.com/cenkalti/backoff/v4 v4.2.1 github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d -github.com/cespare/xxhash/v2 v2.2.0 +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.2 +github.com/cpuguy83/go-md2man/v2 v2.0.4 github.com/creachadair/jrpc2 v1.1.0 github.com/creachadair/mds v0.0.1 github.com/creack/pty v1.1.9 @@ -67,25 +68,24 @@ github.com/getsentry/sentry-go v0.28.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 -github.com/go-chi/chi/v5 v5.0.10 -github.com/go-chi/httprate v0.8.0 +github.com/go-chi/chi/v5 v5.1.0 +github.com/go-chi/httprate v0.12.0 github.com/go-errors/errors v1.5.1 github.com/go-gorp/gorp/v3 v3.1.0 github.com/go-kit/log v0.2.1 -github.com/go-logfmt/logfmt v0.5.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 -github.com/go-sql-driver/mysql v1.6.0 -github.com/gobuffalo/logger v1.0.6 +github.com/go-sql-driver/mysql v1.8.1 github.com/gobuffalo/packd v1.0.2 -github.com/gobuffalo/packr/v2 v2.8.3 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d github.com/goccy/go-json v0.9.11 -github.com/godror/godror v0.24.2 +github.com/godror/godror v0.40.4 +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 @@ -132,13 +132,12 @@ github.com/iris-contrib/schema v0.0.6 github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31 github.com/jmespath/go-jmespath v0.4.0 github.com/jmespath/go-jmespath/internal/testify v1.5.1 -github.com/jmoiron/sqlx v1.3.5 +github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/josharian/intern v1.0.0 github.com/jpillora/backoff v1.0.0 github.com/json-iterator/go v1.1.12 github.com/julienschmidt/httprouter v1.3.0 -github.com/karrick/godirwalk v1.16.1 github.com/kataras/blocks v0.0.7 github.com/kataras/golog v0.1.8 github.com/kataras/iris/v12 v12.2.0 @@ -162,14 +161,11 @@ github.com/mailgun/raymond/v2 v2.0.48 github.com/mailru/easyjson v0.7.7 github.com/manifoldco/promptui v0.9.0 github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 -github.com/markbates/errx v1.1.0 -github.com/markbates/oncer v1.0.0 -github.com/markbates/safe v1.0.1 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-oci8 v0.1.1 github.com/mattn/go-runewidth v0.0.15 -github.com/mattn/go-sqlite3 v1.14.15 +github.com/mattn/go-sqlite3 v1.14.22 github.com/matttproud/golang_protobuf_extensions v1.0.4 github.com/microcosm-cc/bluemonday v1.0.23 github.com/mitchellh/cli v1.1.5 @@ -188,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.1.8 +github.com/nyaruka/phonenumbers v1.4.0 github.com/olekukonko/tablewriter v0.0.5 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.10 @@ -203,14 +199,14 @@ 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.17.0 +github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_model v0.5.0 -github.com/prometheus/common v0.44.0 +github.com/prometheus/common v0.48.0 github.com/prometheus/procfs v0.12.0 github.com/rivo/uniseg v0.2.0 github.com/rogpeppe/go-internal v1.11.0 github.com/rs/cors v1.11.0 -github.com/rubenv/sql-migrate v1.5.2 +github.com/rubenv/sql-migrate v1.7.0 github.com/russross/blackfriday/v2 v2.1.0 github.com/sagikazarmark/crypt v0.19.0 github.com/sagikazarmark/locafero v0.4.0 @@ -218,7 +214,7 @@ github.com/sagikazarmark/slog-shim v0.1.0 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.46 +github.com/segmentio/kafka-go v0.4.47 github.com/sergi/go-diff v1.2.0 github.com/shopspring/decimal v1.3.1 github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c @@ -226,7 +222,7 @@ github.com/sirupsen/logrus v1.9.3 github.com/sourcegraph/conc v0.3.0 github.com/spf13/afero v1.11.0 github.com/spf13/cast v1.6.0 -github.com/spf13/cobra v1.7.0 +github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043 @@ -237,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.11.0 +github.com/twilio/twilio-go v1.22.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 @@ -274,17 +270,17 @@ 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.22.0 -golang.org/x/exp v0.0.0-20231006140011-7918f672742d -golang.org/x/mod v0.13.0 +golang.org/x/crypto v0.25.0 +golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d +golang.org/x/mod v0.17.0 golang.org/x/net v0.24.0 golang.org/x/oauth2 v0.20.0 golang.org/x/sync v0.7.0 -golang.org/x/sys v0.19.0 -golang.org/x/term v0.19.0 -golang.org/x/text v0.14.0 +golang.org/x/sys v0.22.0 +golang.org/x/term v0.22.0 +golang.org/x/text v0.16.0 golang.org/x/time v0.5.0 -golang.org/x/tools v0.14.0 +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 google.golang.org/api v0.177.0 google.golang.org/appengine v1.6.8 diff --git a/go.mod b/go.mod index f1fc6478d..f13996119 100644 --- a/go.mod +++ b/go.mod @@ -5,59 +5,57 @@ go 1.22.1 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.45.26 + github.com/aws/aws-sdk-go v1.55.5 github.com/getsentry/sentry-go v0.28.1 github.com/go-chi/chi v4.1.2+incompatible - github.com/go-chi/chi/v5 v5.0.10 - github.com/go-chi/httprate v0.8.0 + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/httprate v0.12.0 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 github.com/gorilla/schema v1.4.1 - github.com/jmoiron/sqlx v1.3.5 + github.com/jmoiron/sqlx v1.4.0 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.1.8 - github.com/prometheus/client_golang v1.17.0 + github.com/nyaruka/phonenumbers v1.4.0 + github.com/prometheus/client_golang v1.19.1 github.com/rs/cors v1.11.0 - github.com/rubenv/sql-migrate v1.5.2 - github.com/segmentio/kafka-go v0.4.46 + github.com/rubenv/sql-migrate v1.7.0 + github.com/segmentio/kafka-go v0.4.47 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.1 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.11.0 - golang.org/x/crypto v0.22.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d + github.com/twilio/twilio-go v1.22.3 + golang.org/x/crypto v0.25.0 + golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d ) require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.17.7 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -72,8 +70,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.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.34.1 // 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 3a64e19f0..a40832c56 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= @@ -8,13 +10,13 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/aws/aws-sdk-go v1.45.26 h1:PJ2NJNY5N/yeobLYe1Y+xLdavBi67ZI8gvph6ftwVCg= -github.com/aws/aws-sdk-go v1.45.26/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -24,7 +26,7 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -42,22 +44,16 @@ github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRt github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= 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.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/httprate v0.8.0 h1:CyKng28yhGnlGXH9EDGC/Qizj29afJQSNW15W/yj34o= -github.com/go-chi/httprate v0.8.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/httprate v0.12.0 h1:08D/te3pOTJe5+VAZTQrHxwdsH2NyliiUoRD1naKaMg= +github.com/go-chi/httprate v0.12.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= -github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= -github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw= -github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= -github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= -github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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= @@ -65,9 +61,6 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW github.com/golang-jwt/jwt/v4 v4.5.0/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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -88,12 +81,10 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= @@ -104,7 +95,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= @@ -115,17 +105,8 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= -github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= -github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= -github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= -github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ= @@ -133,8 +114,8 @@ github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db/go.mod h1:8UbvGypXm 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.1.8 h1:mjFu85FeoH2Wy18aOMUvxqi1GgAqiQSJsa/cCC5yu2s= -github.com/nyaruka/phonenumbers v1.1.8/go.mod h1:DC7jZd321FqUe+qWSNcHi10tyIyGNXGcNbfkPvdp1Vs= +github.com/nyaruka/phonenumbers v1.4.0 h1:ddhWiHnHCIX3n6ETDA58Zq5dkxkjlvgrDWM2OHHPCzU= +github.com/nyaruka/phonenumbers v1.4.0/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= @@ -153,20 +134,20 @@ 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.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= -github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= -github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= +github.com/rubenv/sql-migrate v1.7.0 h1:HtQq1xyTN2ISmQDggnh0c9U3JlP8apWh8YO2jzlXpTI= +github.com/rubenv/sql-migrate v1.7.0/go.mod h1:S4wtDEG1CKn+0ShpTtzWhFpHHI5PvCUtiGI+C+Z2THE= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -174,8 +155,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= -github.com/segmentio/kafka-go v0.4.46 h1:Sx8/kvtY+/G8nM0roTNnFezSJj3bT2sW0Xy/YY3CgBI= -github.com/segmentio/kafka-go v0.4.46/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= +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/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= @@ -186,8 +167,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -210,8 +191,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.11.0 h1:ixO2DfAV4c0Yza0Tom5F5ZZB8WUbigiFc9wD84vbYnc= -github.com/twilio/twilio-go v1.11.0/go.mod h1:tdnfQ5TjbewoAu4lf9bMsGvfuJ/QU9gYuv9yx3TSIXU= +github.com/twilio/twilio-go v1.22.3 h1:u+h5ywaFd2kGO/36PkizX4N/g5q842cjQQcqZqm6rCo= +github.com/twilio/twilio-go v1.22.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= @@ -244,10 +225,10 @@ 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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +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= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -256,17 +237,17 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -278,30 +259,25 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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 eb8e59ac9ff328fabf51910bee33605cf7d988e7 Mon Sep 17 00:00:00 2001 From: Marcelo Salloum dos Santos Date: Fri, 2 Aug 2024 16:53:53 -0700 Subject: [PATCH 06/75] [SDP-1292] Replace usage of `docker-compose` with `docker compose` (#383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What Replace usage of `docker-compose` with `docker compose` in the CI workflows and the `*.sh` scripts. ### Why `docker-compose`, AKA Compose V1, stopped receiving updates on July 2023 and the [Docker official documentation says](https://docs.docker.com/compose/migrate/#what-are-the-differences-between-compose-v1-and-compose-v2) it should be replaced by `docker compose`, AKA Compose V2: Screenshot 2024-08-02 at 3 32 16โ€ฏPM Since the versions of our CI actions were updated by dependabot, our CI jobs that used docker-compose started failing, surfacing the problem now that the latest docker versions stopped supporting docker-compose. --- .../anchor_platform_integration_check.yml | 4 ++-- .github/workflows/ci.yml | 2 -- .github/workflows/e2e_integration_test.yml | 7 ++++--- ...ngletenant_to_multitenant_db_migration_test.yml | 8 ++++---- dev/docker-compose-tss.yml | 2 +- dev/main.sh | 14 +++++++------- internal/data/disbursement_instructions.go | 4 +++- .../docker/docker-compose-e2e-tests.yml | 2 +- .../scripts/e2e_integration_test.sh | 2 +- ...ingletenant_to_multitenant_db_migration_test.sh | 2 +- internal/serve/httphandler/disbursement_handler.go | 3 ++- internal/serve/serve.go | 6 +++++- .../database_migration_compatibility.sh | 4 ++-- 13 files changed, 33 insertions(+), 27 deletions(-) diff --git a/.github/workflows/anchor_platform_integration_check.yml b/.github/workflows/anchor_platform_integration_check.yml index 718683fe9..8ccd9f7c7 100644 --- a/.github/workflows/anchor_platform_integration_check.yml +++ b/.github/workflows/anchor_platform_integration_check.yml @@ -28,7 +28,7 @@ jobs: - 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 down && docker compose -f docker-compose-sdp-anchor.yml up --build -d - name: Install curl run: sudo apt-get update && sudo apt-get install -y curl @@ -55,4 +55,4 @@ jobs: - name: Docker logs if: always() working-directory: dev - run: docker-compose -f docker-compose-sdp-anchor.yml logs && docker-compose -f docker-compose-sdp-anchor.yml down + run: docker compose -f docker-compose-sdp-anchor.yml logs && docker compose -f docker-compose-sdp-anchor.yml down diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f8cfecaa..f793535b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,6 @@ jobs: with: version: v1.56.2 # this is the golangci-lint version args: --timeout 5m0s - skip-build-cache: true - skip-pkg-cache: true - name: Run ./gomod.sh run: ./gomod.sh diff --git a/.github/workflows/e2e_integration_test.yml b/.github/workflows/e2e_integration_test.yml index 585a02ba0..4a5ad80e9 100644 --- a/.github/workflows/e2e_integration_test.yml +++ b/.github/workflows/e2e_integration_test.yml @@ -26,6 +26,7 @@ jobs: e2e: runs-on: ubuntu-latest strategy: + max-parallel: 1 matrix: platform: - "Stellar" @@ -46,12 +47,12 @@ jobs: - name: Cleanup data working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml down -v + run: docker compose -f docker-compose-e2e-tests.yml down -v 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 --build -V -d shell: bash - name: Install curl @@ -98,5 +99,5 @@ jobs: - name: Docker logs if: always() working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml logs && docker-compose -f docker-compose-e2e-tests.yml down + run: docker compose -f docker-compose-e2e-tests.yml logs && docker compose -f docker-compose-e2e-tests.yml down shell: bash diff --git a/.github/workflows/singletenant_to_multitenant_db_migration_test.yml b/.github/workflows/singletenant_to_multitenant_db_migration_test.yml index 3c0d78de3..6b9b0d2e2 100644 --- a/.github/workflows/singletenant_to_multitenant_db_migration_test.yml +++ b/.github/workflows/singletenant_to_multitenant_db_migration_test.yml @@ -31,12 +31,12 @@ jobs: - name: Cleanup data working-directory: internal/integrationtests/docker - run: docker-compose -f docker-compose-e2e-tests.yml down -v + run: docker compose -f docker-compose-e2e-tests.yml down -v 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 --build -V -d shell: bash - name: Install curl @@ -140,6 +140,6 @@ jobs: if: always() working-directory: internal/integrationtests/docker run: | - docker-compose -f docker-compose-e2e-tests.yml logs - docker-compose -f docker-compose-e2e-tests.yml down -v + docker compose -f docker-compose-e2e-tests.yml logs + docker compose -f docker-compose-e2e-tests.yml down -v shell: bash diff --git a/dev/docker-compose-tss.yml b/dev/docker-compose-tss.yml index 3c101024e..fd562bad4 100644 --- a/dev/docker-compose-tss.yml +++ b/dev/docker-compose-tss.yml @@ -13,7 +13,7 @@ services: NETWORK_PASSPHRASE: "Test SDF Network ; September 2015" HORIZON_URL: "https://horizon-testnet.stellar.org" NUM_CHANNEL_ACCOUNTS: "3" - MAX_BASE_FEE: "100" + MAX_BASE_FEE: "1000000" TSS_METRICS_PORT: "9002" TSS_METRICS_TYPE: "TSS_PROMETHEUS" DISTRIBUTION_PUBLIC_KEY: ${DISTRIBUTION_PUBLIC_KEY} diff --git a/dev/main.sh b/dev/main.sh index 15d6699ce..ac2dd5781 100755 --- a/dev/main.sh +++ b/dev/main.sh @@ -34,11 +34,11 @@ fi # prepare echo $DIVIDER -echo "====> ๐Ÿ‘€ start calling docker-compose -p sdp-multi-tenant down" +echo "====> ๐Ÿ‘€ start calling docker compose -p sdp-multi-tenant down" docker ps -aq | xargs docker stop | xargs docker rm -#docker-compose -p sdp-multi-tenant down -docker-compose down -echo "====> โœ… finish calling docker-compose down" +#docker compose -p sdp-multi-tenant down +docker compose down +echo "====> โœ… finish calling docker compose down" # Run docker compose echo $DIVIDER @@ -67,11 +67,11 @@ fi echo $DIVIDER echo "====> ๐Ÿ‘€calling docker compose up" export GIT_COMMIT="debug" -docker-compose -p sdp-multi-tenant up -d --build +docker compose -p sdp-multi-tenant up -d --build # Run docker compose echo $DIVIDER -echo "====> โœ…finish calling docker-compose up" +echo "====> โœ…finish calling docker compose up" # Initialize tenants @@ -144,7 +144,7 @@ echo "====> โœ…Step 3: finished initialization of tenants" echo $DIVIDER # Initialize test_users echo "====> Step 4: initialize test users..." -docker-compose -p sdp-multi-tenant exec sdp-api ./dev/scripts/add_test_users.sh +docker compose -p sdp-multi-tenant exec sdp-api ./dev/scripts/add_test_users.sh echo $DIVIDER echo "๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ SUCCESS! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰" diff --git a/internal/data/disbursement_instructions.go b/internal/data/disbursement_instructions.go index c7493c979..2281a2df4 100644 --- a/internal/data/disbursement_instructions.go +++ b/internal/data/disbursement_instructions.go @@ -65,7 +65,7 @@ var ( // | | |--- 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. -// | | |--- Delete all payments tied to this disbursement. +// | | |--- Delete all previously existing payments tied to this disbursement. // | | |--- Create all payments passed in the instructions. func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID string, instructions []*DisbursementInstruction, disbursement *Disbursement, update *DisbursementUpdate, maxNumberOfInstructions int) error { if len(instructions) > maxNumberOfInstructions { @@ -85,6 +85,7 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID st return fmt.Errorf("error fetching receivers by phone number: %w", err) } + // Create a map of existing receivers for easy lookup receiverMap := make(map[string]*Receiver) for _, receiver := range existingReceivers { receiverMap[receiver.PhoneNumber] = receiver @@ -98,6 +99,7 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID st } } + // Create missing receivers for _, instruction := range instructions { _, exists := receiverMap[instruction.Phone] if !exists { diff --git a/internal/integrationtests/docker/docker-compose-e2e-tests.yml b/internal/integrationtests/docker/docker-compose-e2e-tests.yml index b4547721b..11f5e81c8 100644 --- a/internal/integrationtests/docker/docker-compose-e2e-tests.yml +++ b/internal/integrationtests/docker/docker-compose-e2e-tests.yml @@ -124,7 +124,7 @@ services: NETWORK_PASSPHRASE: "Test SDF Network ; September 2015" HORIZON_URL: "https://horizon-testnet.stellar.org" NUM_CHANNEL_ACCOUNTS: "1" - MAX_BASE_FEE: "100000" + MAX_BASE_FEE: "1000000" TSS_METRICS_PORT: "9002" TSS_METRICS_TYPE: "TSS_PROMETHEUS" DISTRIBUTION_PUBLIC_KEY: ${DISTRIBUTION_PUBLIC_KEY} diff --git a/internal/integrationtests/scripts/e2e_integration_test.sh b/internal/integrationtests/scripts/e2e_integration_test.sh index 185ad74bb..444d7decf 100755 --- a/internal/integrationtests/scripts/e2e_integration_test.sh +++ b/internal/integrationtests/scripts/e2e_integration_test.sh @@ -41,7 +41,7 @@ for accountType in "${accountTypes[@]}"; do # Run docker compose echo $DIVIDER echo "====> ๐Ÿ‘€Step 2: build sdp-api, anchor-platform and tss" - docker-compose -f ../docker/docker-compose-e2e-tests.yml up --build -d + docker compose -f ../docker/docker-compose-e2e-tests.yml up --build -d wait_for_server "http://localhost:8000/health" 20 echo "====> โœ…Step 2: finishing build" diff --git a/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh b/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh index 41e4e98c4..46b11c334 100755 --- a/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh +++ b/internal/integrationtests/scripts/singletenant_to_multitenant_db_migration_test.sh @@ -33,7 +33,7 @@ echo "====> โœ…Step 1: finish preparation" # Run docker compose echo $DIVIDER echo "====> ๐Ÿ‘€Step 2: build sdp-api, anchor-platform and tss" -docker-compose -f ../docker/docker-compose-e2e-tests.yml up --build -d +docker compose -f ../docker/docker-compose-e2e-tests.yml up --build -d wait_for_server "http://localhost:8000/health" 20 echo "====> โœ…Step 2: finishing build" diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go index b6107305b..52b187ddc 100644 --- a/internal/serve/httphandler/disbursement_handler.go +++ b/internal/serve/httphandler/disbursement_handler.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "slices" "time" "github.com/go-chi/chi/v5" @@ -203,7 +204,7 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter, } // check if disbursement is in draft, ready status - if disbursement.Status != data.DraftDisbursementStatus && disbursement.Status != data.ReadyDisbursementStatus { + if !slices.Contains([]data.DisbursementStatus{data.DraftDisbursementStatus, data.ReadyDisbursementStatus}, disbursement.Status) { httperror.BadRequest("disbursement is not in draft or ready status", nil, nil).Render(w) return } diff --git a/internal/serve/serve.go b/internal/serve/serve.go index dff12a2b0..77296800c 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -469,7 +469,11 @@ func handleHTTP(o ServeOptions) *chi.Mux { }.ServeHTTP) // This loads the SEP-24 PII registration webpage. sep24HeaderTokenAuthenticationMiddleware := anchorplatform.SEP24HeaderTokenAuthenticateMiddleware(o.sep24JWTManager, o.NetworkPassphrase, o.tenantManager, o.SingleTenantMode) - r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/otp", httphandler.ReceiverSendOTPHandler{Models: o.Models, SMSMessengerClient: o.SMSMessengerClient, ReCAPTCHAValidator: reCAPTCHAValidator}.ServeHTTP) + r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/otp", httphandler.ReceiverSendOTPHandler{ + Models: o.Models, + SMSMessengerClient: o.SMSMessengerClient, + ReCAPTCHAValidator: reCAPTCHAValidator, + }.ServeHTTP) r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/verification", httphandler.VerifyReceiverRegistrationHandler{ AnchorPlatformAPIService: o.AnchorPlatformAPIService, Models: o.Models, diff --git a/v1_compatibility/database_migration_compatibility.sh b/v1_compatibility/database_migration_compatibility.sh index a81f9bdb8..632cf929a 100755 --- a/v1_compatibility/database_migration_compatibility.sh +++ b/v1_compatibility/database_migration_compatibility.sh @@ -17,8 +17,8 @@ echo "====> โœ…Step 1: finish cloning SDP v1 (stellar/stellar-relief-backoffice- # Run docker compose echo $DIVIDER echo "====> ๐Ÿ‘€Step 2: start calling docker compose up" -docker compose down && docker-compose up --abort-on-container-exit -echo "====> โœ…Step 2: finish calling docker-compose up" +docker compose down && docker compose up --abort-on-container-exit +echo "====> โœ…Step 2: finish calling docker compose up" echo $DIVIDER echo "๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ SUCCESS! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰" \ No newline at end of file From fd5629dbe57fa63687f3a8ba70d7e5c8c63bbb63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:33:38 +0000 Subject: [PATCH 07/75] Bump docker/build-push-action in the all-actions group (#388) --- .github/workflows/docker_image_public_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_image_public_release.yml b/.github/workflows/docker_image_public_release.yml index fc91ec4cf..307125873 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.5.0 + uses: docker/build-push-action@v6.6.1 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.5.0 + uses: docker/build-push-action@v6.6.1 with: push: true build-args: | From 99cbb5d4e2fb89dc5f0c08c8f4e0955600e08a82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:53:26 +0000 Subject: [PATCH 08/75] Bump golang in the all-docker group (#387) --- Dockerfile | 2 +- Dockerfile.development | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 52f64e8c4..af6e7a18e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # To push: # make docker-push -FROM golang:1.22.5-bullseye AS build +FROM golang:1.22.6-bullseye AS build ARG GIT_COMMIT WORKDIR /src/stellar-disbursement-platform diff --git a/Dockerfile.development b/Dockerfile.development index a6e04efac..cc25b9004 100644 --- a/Dockerfile.development +++ b/Dockerfile.development @@ -1,5 +1,5 @@ # Stage 1: Build the Go application -FROM golang:1.22.5-bullseye AS build +FROM golang:1.22.6-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.22.5-bullseye AS development +FROM golang:1.22.6-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 3f16719a909ef8706a4605f39d176fd780eeb99a Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Thu, 15 Aug 2024 14:49:22 -0700 Subject: [PATCH 09/75] [SDP-1317] Add MessageDispatcher to SDP (#391) --- cmd/serve.go | 14 +- cmd/serve_test.go | 15 ++- .../dependencyinjection/message_dispatcher.go | 45 +++++++ .../message_dispatcher_test.go | 116 +++++++++++++++++ ...er_wallets_sms_invitation_event_handler.go | 4 +- internal/message/message_dispatcher.go | 57 ++++++++ internal/message/message_dispatcher_test.go | 122 ++++++++++++++++++ internal/message/messenger_client.go | 1 + .../mock_message_dispatcher_interface.go | 81 ++++++++++++ internal/message/mock_messenger_client.go | 60 +++++++++ internal/message/mocks.go | 21 --- ...end_receiver_wallets_sms_invitation_job.go | 4 +- ...eceiver_wallets_sms_invitation_job_test.go | 31 +++-- .../httphandler/receiver_send_otp_handler.go | 7 +- .../receiver_send_otp_handler_test.go | 12 +- internal/serve/serve.go | 4 +- internal/serve/serve_test.go | 8 +- .../send_receiver_wallets_invite_service.go | 26 ++-- ...nd_receiver_wallets_invite_service_test.go | 65 +++++----- 19 files changed, 597 insertions(+), 96 deletions(-) create mode 100644 internal/dependencyinjection/message_dispatcher.go create mode 100644 internal/dependencyinjection/message_dispatcher_test.go create mode 100644 internal/message/message_dispatcher.go create mode 100644 internal/message/message_dispatcher_test.go create mode 100644 internal/message/mock_message_dispatcher_interface.go create mode 100644 internal/message/mock_messenger_client.go delete mode 100644 internal/message/mocks.go diff --git a/cmd/serve.go b/cmd/serve.go index 6d8f7230c..48db2925a 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -118,7 +118,7 @@ func (s *ServerService) GetSchedulerJobRegistrars( scheduler.WithPatchAnchorPlatformTransactionsCompletionJobOption(schedulerOptions.PaymentJobIntervalSeconds, apAPIService, models), scheduler.WithSendReceiverWalletsSMSInvitationJobOption(jobs.SendReceiverWalletsSMSInvitationJobOptions{ Models: models, - MessengerClient: serveOpts.SMSMessengerClient, + MessageDispatcher: serveOpts.MessageDispatcher, MaxInvitationSMSResendAttempts: int64(serveOpts.MaxInvitationSMSResendAttempts), Sep10SigningPrivateKey: serveOpts.Sep10SigningPrivateKey, CrashTrackerClient: serveOpts.CrashTrackerClient.Clone(), @@ -147,7 +147,7 @@ func (s *ServerService) SetupConsumers(ctx context.Context, o SetupConsumersOpti MtnDBConnectionPool: o.ServeOpts.MtnDBConnectionPool, AdminDBConnectionPool: o.ServeOpts.AdminDBConnectionPool, AnchorPlatformBaseSepURL: o.ServeOpts.AnchorPlatformBasePlatformURL, - MessengerClient: o.ServeOpts.SMSMessengerClient, + MessageDispatcher: o.ServeOpts.MessageDispatcher, MaxInvitationSMSResendAttempts: int64(o.ServeOpts.MaxInvitationSMSResendAttempts), Sep10SigningPrivateKey: o.ServeOpts.Sep10SigningPrivateKey, }), @@ -582,10 +582,14 @@ func (c *ServeCommand) Command(serverService ServerServiceInterface, monitorServ serveOpts.EmailMessengerClient = emailMessengerClient adminServeOpts.EmailMessengerClient = emailMessengerClient - // Setup the SMS client - serveOpts.SMSMessengerClient, err = di.NewSMSClient(smsOpts) + // Setup the Message Dispatcher + messageDispatcherOpts := di.MessageDispatcherOpts{ + EmailOpts: &emailOpts, + SMSOpts: &smsOpts, + } + serveOpts.MessageDispatcher, err = di.NewMessageDispatcher(ctx, messageDispatcherOpts) if err != nil { - log.Ctx(ctx).Fatalf("error creating SMS client: %s", err.Error()) + log.Ctx(ctx).Fatalf("error creating message dispatcher: %s", err.Error()) } // Setup the AP Auth enforcer diff --git a/cmd/serve_test.go b/cmd/serve_test.go index 23626a2e2..05a4ac038 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -190,11 +190,18 @@ func Test_serve(t *testing.T) { require.NoError(t, err) serveOpts.CrashTrackerClient = crashTrackerClient - messengerClient, err := di.NewEmailClient(di.EmailClientOptions{EmailType: message.MessengerTypeDryRun}) + emailOpts := di.EmailClientOptions{EmailType: message.MessengerTypeDryRun} + emailClient, err := di.NewEmailClient(emailOpts) require.NoError(t, err) - serveOpts.EmailMessengerClient = messengerClient + serveOpts.EmailMessengerClient = emailClient - serveOpts.SMSMessengerClient, err = di.NewSMSClient(di.SMSClientOptions{SMSType: message.MessengerTypeDryRun}) + smsOpts := di.SMSClientOptions{SMSType: message.MessengerTypeDryRun} + + messageDispatcherOpts := di.MessageDispatcherOpts{ + EmailOpts: &emailOpts, + SMSOpts: &smsOpts, + } + serveOpts.MessageDispatcher, err = di.NewMessageDispatcher(ctx, messageDispatcherOpts) require.NoError(t, err) kafkaConfig := events.KafkaConfig{ @@ -220,7 +227,7 @@ func Test_serve(t *testing.T) { serveTenantOpts := serveadmin.ServeOptions{ Environment: "test", - EmailMessengerClient: messengerClient, + EmailMessengerClient: emailClient, AdminDBConnectionPool: dbConnectionPool, MTNDBConnectionPool: dbConnectionPool, CrashTrackerClient: crashTrackerClient, diff --git a/internal/dependencyinjection/message_dispatcher.go b/internal/dependencyinjection/message_dispatcher.go new file mode 100644 index 000000000..09d82629a --- /dev/null +++ b/internal/dependencyinjection/message_dispatcher.go @@ -0,0 +1,45 @@ +package dependencyinjection + +import ( + "context" + "fmt" + + "github.com/stellar/stellar-disbursement-platform-backend/internal/message" +) + +const MessageDispatcherInstanceName = "message_dispatcher_instance" + +type MessageDispatcherOpts struct { + EmailOpts *EmailClientOptions + SMSOpts *SMSClientOptions +} + +func NewMessageDispatcher(ctx context.Context, opts MessageDispatcherOpts) (*message.MessageDispatcher, error) { + if instance, ok := GetInstance(MessageDispatcherInstanceName); ok { + if dispatcherInstance, ok := instance.(*message.MessageDispatcher); ok { + return dispatcherInstance, nil + } + return nil, fmt.Errorf("trying to cast pre-existing MessageDispatcher for dependency injection") + } + + dispatcher := message.NewMessageDispatcher() + + if opts.EmailOpts != nil { + emailClient, err := NewEmailClient(*opts.EmailOpts) + if err != nil { + return nil, fmt.Errorf("creating email client: %w", err) + } + dispatcher.RegisterClient(ctx, message.MessageChannelEmail, emailClient) + } + + if opts.SMSOpts != nil { + smsClient, err := NewSMSClient(*opts.SMSOpts) + if err != nil { + return nil, fmt.Errorf("creating SMS client: %w", err) + } + dispatcher.RegisterClient(ctx, message.MessageChannelSMS, smsClient) + } + + SetInstance(MessageDispatcherInstanceName, dispatcher) + return dispatcher, nil +} diff --git a/internal/dependencyinjection/message_dispatcher_test.go b/internal/dependencyinjection/message_dispatcher_test.go new file mode 100644 index 000000000..e60c02741 --- /dev/null +++ b/internal/dependencyinjection/message_dispatcher_test.go @@ -0,0 +1,116 @@ +package dependencyinjection + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/stellar-disbursement-platform-backend/internal/message" +) + +func Test_NewMessageDispatcher(t *testing.T) { + ctx := context.Background() + t.Run("should return the same instance when called twice", func(t *testing.T) { + defer ClearInstancesTestHelper(t) + + opts := MessageDispatcherOpts{ + EmailOpts: &EmailClientOptions{ + EmailType: message.MessengerTypeDryRun, + }, + SMSOpts: &SMSClientOptions{ + SMSType: message.MessengerTypeDryRun, + }, + } + + dispatcher1, err := NewMessageDispatcher(ctx, opts) + require.NoError(t, err) + dispatcher2, err := NewMessageDispatcher(ctx, opts) + require.NoError(t, err) + assert.Equal(t, dispatcher1, dispatcher2) + }) + + t.Run("should create dispatcher with email client only", func(t *testing.T) { + defer ClearInstancesTestHelper(t) + + opts := MessageDispatcherOpts{ + EmailOpts: &EmailClientOptions{ + EmailType: message.MessengerTypeDryRun, + }, + } + + dispatcher, err := NewMessageDispatcher(ctx, opts) + require.NoError(t, err) + + emailClient, err := dispatcher.GetClient(message.MessageChannelEmail) + require.NoError(t, err) + assert.NotNil(t, emailClient) + + smsClient, err := dispatcher.GetClient(message.MessageChannelSMS) + assert.EqualError(t, err, "no client registered for channel \"SMS\"") + assert.Nil(t, smsClient) + }) + + t.Run("should create dispatcher with SMS client only", func(t *testing.T) { + defer ClearInstancesTestHelper(t) + + opts := MessageDispatcherOpts{ + SMSOpts: &SMSClientOptions{ + SMSType: message.MessengerTypeDryRun, + }, + } + + dispatcher, err := NewMessageDispatcher(ctx, opts) + require.NoError(t, err) + + smsClient, err := dispatcher.GetClient(message.MessageChannelSMS) + require.NoError(t, err) + assert.NotNil(t, smsClient) + + emailClient, err := dispatcher.GetClient(message.MessageChannelEmail) + assert.EqualError(t, err, "no client registered for channel \"EMAIL\"") + assert.Nil(t, emailClient) + }) + + t.Run("should return an error on invalid email client creation", func(t *testing.T) { + defer ClearInstancesTestHelper(t) + + opts := MessageDispatcherOpts{ + EmailOpts: &EmailClientOptions{ + EmailType: "invalid-type", + }, + } + + dispatcher, err := NewMessageDispatcher(ctx, opts) + assert.ErrorContains(t, err, `trying to create a Email client with a non-supported Email type: "invalid-type"`) + assert.Nil(t, dispatcher) + }) + + t.Run("should return an error on invalid SMS client creation", func(t *testing.T) { + defer ClearInstancesTestHelper(t) + + opts := MessageDispatcherOpts{ + SMSOpts: &SMSClientOptions{ + SMSType: "invalid-type", + }, + } + + dispatcher, err := NewMessageDispatcher(ctx, opts) + assert.ErrorContains(t, err, `trying to create a SMS client with a non-supported SMS type: "invalid-type"`) + assert.Nil(t, dispatcher) + }) + + t.Run("should return an error on invalid pre-existing instance", func(t *testing.T) { + defer ClearInstancesTestHelper(t) + + preExistingDispatcherWithInvalidType := struct{}{} + SetInstance(MessageDispatcherInstanceName, preExistingDispatcherWithInvalidType) + + opts := MessageDispatcherOpts{} + + gotDispatcher, err := NewMessageDispatcher(ctx, opts) + assert.Nil(t, gotDispatcher) + assert.EqualError(t, err, "trying to cast pre-existing MessageDispatcher for dependency injection") + }) +} diff --git a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go b/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go index 854e5801b..3fc6e29a5 100644 --- a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go +++ b/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go @@ -21,7 +21,7 @@ type SendReceiverWalletsSMSInvitationEventHandlerOptions struct { AdminDBConnectionPool db.DBConnectionPool MtnDBConnectionPool db.DBConnectionPool AnchorPlatformBaseSepURL string - MessengerClient message.MessengerClient + MessageDispatcher message.MessageDispatcherInterface MaxInvitationSMSResendAttempts int64 Sep10SigningPrivateKey string CrashTrackerClient crashtracker.CrashTrackerClient @@ -45,7 +45,7 @@ func NewSendReceiverWalletsSMSInvitationEventHandler(options SendReceiverWallets s, err := services.NewSendReceiverWalletInviteService( models, - options.MessengerClient, + options.MessageDispatcher, options.Sep10SigningPrivateKey, options.MaxInvitationSMSResendAttempts, options.CrashTrackerClient, diff --git a/internal/message/message_dispatcher.go b/internal/message/message_dispatcher.go new file mode 100644 index 000000000..582e00745 --- /dev/null +++ b/internal/message/message_dispatcher.go @@ -0,0 +1,57 @@ +package message + +import ( + "context" + "fmt" + + "github.com/stellar/go/support/log" +) + +type MessageChannel string + +const ( + MessageChannelEmail MessageChannel = "EMAIL" + MessageChannelSMS MessageChannel = "SMS" +) + +//go:generate mockery --name MessageDispatcherInterface --case=underscore --structname=MockMessageDispatcher --inpackage +type MessageDispatcherInterface interface { + RegisterClient(ctx context.Context, channel MessageChannel, client MessengerClient) + SendMessage(message Message, channel MessageChannel) error + GetClient(channel MessageChannel) (MessengerClient, error) +} + +type MessageDispatcher struct { + clients map[MessageChannel]MessengerClient +} + +func NewMessageDispatcher() *MessageDispatcher { + return &MessageDispatcher{ + clients: make(map[MessageChannel]MessengerClient), + } +} + +func (d *MessageDispatcher) RegisterClient(ctx context.Context, channel MessageChannel, client MessengerClient) { + log.Ctx(ctx).Infof("๐Ÿ“ก [MessageDispatcher] Registering client %s for channel %s", client.MessengerType(), channel) + d.clients[channel] = client +} + +func (d *MessageDispatcher) SendMessage(message Message, channel MessageChannel) error { + client, err := d.GetClient(channel) + if err != nil { + return fmt.Errorf("getting client for channel: %w", err) + } + + return client.SendMessage(message) +} + +func (d *MessageDispatcher) GetClient(channel MessageChannel) (MessengerClient, error) { + client, ok := d.clients[channel] + if !ok { + return nil, fmt.Errorf("no client registered for channel %q", channel) + } + + return client, nil +} + +var _ MessageDispatcherInterface = &MessageDispatcher{} diff --git a/internal/message/message_dispatcher_test.go b/internal/message/message_dispatcher_test.go new file mode 100644 index 000000000..e53637dec --- /dev/null +++ b/internal/message/message_dispatcher_test.go @@ -0,0 +1,122 @@ +package message + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_NewMessageDispatcher(t *testing.T) { + dispatcher := NewMessageDispatcher() + assert.NotNil(t, dispatcher) + assert.Empty(t, dispatcher.clients) +} + +func Test_MessageDispatcher_RegisterClient(t *testing.T) { + ctx := context.Background() + + dispatcher := NewMessageDispatcher() + client := NewMessengerClientMock(t) + client.On("MessengerType").Return(MessengerTypeDryRun).Once() + + dispatcher.RegisterClient(ctx, MessageChannelEmail, client) + + assert.Len(t, dispatcher.clients, 1) + assert.Equal(t, client, dispatcher.clients[MessageChannelEmail]) +} + +func Test_MessageDispatcher_GetClient(t *testing.T) { + ctx := context.Background() + dispatcher := NewMessageDispatcher() + emailClient := NewMessengerClientMock(t) + emailClient.On("MessengerType").Return(MessengerTypeDryRun).Once() + dispatcher.RegisterClient(ctx, MessageChannelEmail, emailClient) + + tests := []struct { + name string + channel MessageChannel + expected MessengerClient + expectedErr error + }{ + { + name: "Existing Email client", + channel: MessageChannelEmail, + expected: emailClient, + expectedErr: nil, + }, + { + name: "Non-existing client", + channel: MessageChannelSMS, + expected: nil, + expectedErr: errors.New("no client registered for channel \"SMS\""), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := dispatcher.GetClient(tt.channel) + assert.Equal(t, tt.expected, result) + if tt.expectedErr != nil { + assert.EqualError(t, err, tt.expectedErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_MessageDispatcher_SendMessage(t *testing.T) { + ctx := context.Background() + + dispatcher := NewMessageDispatcher() + message := Message{} + + tests := []struct { + name string + channel MessageChannel + setupMock func(*MessengerClientMock) + expectedErr error + }{ + { + name: "Successful send", + channel: MessageChannelEmail, + setupMock: func(clientMock *MessengerClientMock) { + clientMock.On("SendMessage", message).Return(nil) + }, + expectedErr: nil, + }, + { + name: "Client not found", + channel: MessageChannelSMS, + setupMock: func(clientMock *MessengerClientMock) {}, + expectedErr: errors.New("getting client for channel: no client registered for channel \"SMS\""), + }, + { + name: "Client error", + channel: MessageChannelEmail, + setupMock: func(clientMock *MessengerClientMock) { + clientMock.On("SendMessage", message).Return(errors.New("send error")) + }, + expectedErr: errors.New("send error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewMessengerClientMock(t) + client.On("MessengerType").Return(MessengerTypeDryRun).Once() + dispatcher.RegisterClient(ctx, MessageChannelEmail, client) + + tt.setupMock(client) + + err := dispatcher.SendMessage(message, tt.channel) + if tt.expectedErr != nil { + assert.EqualError(t, err, tt.expectedErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/message/messenger_client.go b/internal/message/messenger_client.go index 913bedf1a..8fe7cdf4a 100644 --- a/internal/message/messenger_client.go +++ b/internal/message/messenger_client.go @@ -1,5 +1,6 @@ package message +//go:generate mockery --name=MessengerClient --case=underscore --structname=MessengerClientMock --inpackage type MessengerClient interface { SendMessage(message Message) error MessengerType() MessengerType diff --git a/internal/message/mock_message_dispatcher_interface.go b/internal/message/mock_message_dispatcher_interface.go new file mode 100644 index 000000000..763371f31 --- /dev/null +++ b/internal/message/mock_message_dispatcher_interface.go @@ -0,0 +1,81 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package message + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockMessageDispatcher is an autogenerated mock type for the MessageDispatcherInterface type +type MockMessageDispatcher struct { + mock.Mock +} + +// GetClient provides a mock function with given fields: channel +func (_m *MockMessageDispatcher) GetClient(channel MessageChannel) (MessengerClient, error) { + ret := _m.Called(channel) + + if len(ret) == 0 { + panic("no return value specified for GetClient") + } + + var r0 MessengerClient + var r1 error + if rf, ok := ret.Get(0).(func(MessageChannel) (MessengerClient, error)); ok { + return rf(channel) + } + if rf, ok := ret.Get(0).(func(MessageChannel) MessengerClient); ok { + r0 = rf(channel) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(MessengerClient) + } + } + + if rf, ok := ret.Get(1).(func(MessageChannel) error); ok { + r1 = rf(channel) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegisterClient provides a mock function with given fields: ctx, channel, client +func (_m *MockMessageDispatcher) RegisterClient(ctx context.Context, channel MessageChannel, client MessengerClient) { + _m.Called(ctx, channel, client) +} + +// SendMessage provides a mock function with given fields: message, channel +func (_m *MockMessageDispatcher) SendMessage(message Message, channel MessageChannel) error { + ret := _m.Called(message, channel) + + if len(ret) == 0 { + panic("no return value specified for SendMessage") + } + + var r0 error + if rf, ok := ret.Get(0).(func(Message, MessageChannel) error); ok { + r0 = rf(message, channel) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockMessageDispatcher creates a new instance of MockMessageDispatcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockMessageDispatcher(t interface { + mock.TestingT + Cleanup(func()) +}) *MockMessageDispatcher { + mock := &MockMessageDispatcher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/message/mock_messenger_client.go b/internal/message/mock_messenger_client.go new file mode 100644 index 000000000..ecea79645 --- /dev/null +++ b/internal/message/mock_messenger_client.go @@ -0,0 +1,60 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package message + +import mock "github.com/stretchr/testify/mock" + +// MessengerClientMock is an autogenerated mock type for the MessengerClient type +type MessengerClientMock struct { + mock.Mock +} + +// MessengerType provides a mock function with given fields: +func (_m *MessengerClientMock) MessengerType() MessengerType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for MessengerType") + } + + var r0 MessengerType + if rf, ok := ret.Get(0).(func() MessengerType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(MessengerType) + } + + return r0 +} + +// SendMessage provides a mock function with given fields: message +func (_m *MessengerClientMock) SendMessage(message Message) error { + ret := _m.Called(message) + + if len(ret) == 0 { + panic("no return value specified for SendMessage") + } + + var r0 error + if rf, ok := ret.Get(0).(func(Message) error); ok { + r0 = rf(message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMessengerClientMock creates a new instance of MessengerClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMessengerClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *MessengerClientMock { + mock := &MessengerClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/message/mocks.go b/internal/message/mocks.go deleted file mode 100644 index cabd6b72f..000000000 --- a/internal/message/mocks.go +++ /dev/null @@ -1,21 +0,0 @@ -package message - -import ( - "github.com/stretchr/testify/mock" -) - -type MessengerClientMock struct { - mock.Mock -} - -func (mc *MessengerClientMock) SendMessage(message Message) error { - args := mc.Called(message) - return args.Error(0) -} - -func (mc *MessengerClientMock) MessengerType() MessengerType { - args := mc.Called() - return args.Get(0).(MessengerType) -} - -var _ MessengerClient = (*MessengerClientMock)(nil) diff --git a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go index 1af631718..44e9fb8cf 100644 --- a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go +++ b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go @@ -19,7 +19,7 @@ const ( type SendReceiverWalletsSMSInvitationJobOptions struct { Models *data.Models - MessengerClient message.MessengerClient + MessageDispatcher message.MessageDispatcherInterface MaxInvitationSMSResendAttempts int64 Sep10SigningPrivateKey string CrashTrackerClient crashtracker.CrashTrackerClient @@ -59,7 +59,7 @@ func NewSendReceiverWalletsSMSInvitationJob(options SendReceiverWalletsSMSInvita } s, err := services.NewSendReceiverWalletInviteService( options.Models, - options.MessengerClient, + options.MessageDispatcher, options.Sep10SigningPrivateKey, options.MaxInvitationSMSResendAttempts, options.CrashTrackerClient, 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 07b12be90..38d853635 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 @@ -33,8 +33,13 @@ func Test_NewSendReceiverWalletsSMSInvitationJob(t *testing.T) { models, err := data.NewModels(dbConnectionPool) require.NoError(t, err) + ctx := context.Background() + messageDryRunClient, err := message.NewDryRunClient() require.NoError(t, err) + dryRunDispatcher := message.NewMessageDispatcher() + dryRunDispatcher.RegisterClient(ctx, message.MessageChannelSMS, messageDryRunClient) + dryRunDispatcher.RegisterClient(ctx, message.MessageChannelEmail, messageDryRunClient) t.Run("exits with status 1 when Messenger Client is missing config", func(t *testing.T) { if os.Getenv("TEST_FATAL") == "1" { @@ -65,7 +70,7 @@ func Test_NewSendReceiverWalletsSMSInvitationJob(t *testing.T) { if os.Getenv("TEST_FATAL") == "1" { o := SendReceiverWalletsSMSInvitationJobOptions{ Models: models, - MessengerClient: messageDryRunClient, + MessageDispatcher: dryRunDispatcher, MaxInvitationSMSResendAttempts: 3, } @@ -90,7 +95,7 @@ func Test_NewSendReceiverWalletsSMSInvitationJob(t *testing.T) { t.Run("returns a job instance successfully", func(t *testing.T) { o := SendReceiverWalletsSMSInvitationJobOptions{ Models: models, - MessengerClient: messageDryRunClient, + MessageDispatcher: dryRunDispatcher, MaxInvitationSMSResendAttempts: 3, JobIntervalSeconds: DefaultMinimumJobIntervalSeconds, } @@ -133,12 +138,14 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { var maxInvitationSMSResendAttempts int64 = 3 t.Run("executes the service successfully", func(t *testing.T) { - messengerClientMock := &message.MessengerClientMock{} + messageDispatcherMock := message.NewMockMessageDispatcher(t) + messengerClientMock := message.NewMessengerClientMock(t) + crashTrackerClientMock := &crashtracker.MockCrashTrackerClient{} s, err := services.NewSendReceiverWalletInviteService( models, - messengerClientMock, + messageDispatcherMock, stellarSecretKey, maxInvitationSMSResendAttempts, crashTrackerClientMock, @@ -222,22 +229,26 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2) mockErr := errors.New("unexpected error") - messengerClientMock. + messageDispatcherMock. + On("GetClient", message.MessageChannelSMS). + Return(messengerClientMock, nil). + Twice(). On("SendMessage", message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }). + }, message.MessageChannelSMS). Return(mockErr). Once(). On("SendMessage", message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }). + }, message.MessageChannelSMS). Return(nil). - Once(). + Once() + messengerClientMock. On("MessengerType"). - Return(message.MessengerTypeTwilioSMS) - + Return(message.MessengerTypeTwilioSMS). + Twice() 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, diff --git a/internal/serve/httphandler/receiver_send_otp_handler.go b/internal/serve/httphandler/receiver_send_otp_handler.go index b35e0e019..303edc460 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler.go +++ b/internal/serve/httphandler/receiver_send_otp_handler.go @@ -25,7 +25,7 @@ const OTPMessageDisclaimer = " If you did not request this code, please ignore. type ReceiverSendOTPHandler struct { Models *data.Models - SMSMessengerClient message.MessengerClient + MessageDispatcher message.MessageDispatcherInterface ReCAPTCHAValidator validators.ReCAPTCHAValidator } @@ -151,13 +151,14 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } - smsMessage := message.Message{ + msg := message.Message{ ToPhoneNumber: receiverSendOTPRequest.PhoneNumber, Message: builder.String(), } + // TODO: SDP-1296 - support multiple channels for OTP log.Ctx(ctx).Infof("sending OTP message to phone number: %s", truncatedPhoneNumber) - err = h.SMSMessengerClient.SendMessage(smsMessage) + err = h.MessageDispatcher.SendMessage(msg, message.MessageChannelSMS) if err != nil { httperror.InternalError(ctx, "Cannot send OTP message", err, nil).Render(w) return diff --git a/internal/serve/httphandler/receiver_send_otp_handler_test.go b/internal/serve/httphandler/receiver_send_otp_handler_test.go index 5ef742ed5..35138bd3e 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler_test.go +++ b/internal/serve/httphandler/receiver_send_otp_handler_test.go @@ -53,12 +53,12 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.RegisteredReceiversWalletStatus) _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet1.ID, data.RegisteredReceiversWalletStatus) - mockMessenger := message.MessengerClientMock{} + mockMessageDispatcher := message.NewMockMessageDispatcher(t) reCAPTCHAValidator := &validators.ReCAPTCHAValidatorMock{} r.Post("/wallet-registration/otp", ReceiverSendOTPHandler{ Models: models, - SMSMessengerClient: &mockMessenger, + MessageDispatcher: mockMessageDispatcher, ReCAPTCHAValidator: reCAPTCHAValidator, }.ServeHTTP) @@ -168,7 +168,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { } req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - mockMessenger.On("SendMessage", mock.AnythingOfType("message.Message")). + mockMessageDispatcher.On("SendMessage", mock.AnythingOfType("message.Message"), message.MessageChannelSMS). Return(nil). Once(). Run(func(args mock.Arguments) { @@ -212,7 +212,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { err = models.Organizations.Update(ctx, &data.OrganizationUpdate{OTPMessageTemplate: &customOTPMessage}) require.NoError(t, err) - mockMessenger.On("SendMessage", mock.AnythingOfType("message.Message")). + mockMessageDispatcher.On("SendMessage", mock.AnythingOfType("message.Message"), message.MessageChannelSMS). Return(nil). Once(). Run(func(args mock.Arguments) { @@ -251,7 +251,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { } req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - mockMessenger.On("SendMessage", mock.AnythingOfType("message.Message")). + mockMessageDispatcher.On("SendMessage", mock.AnythingOfType("message.Message"), message.MessageChannelSMS). Return(errors.New("error sending message")). Once() @@ -376,6 +376,6 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { assert.JSONEq(t, wantsBody, string(respBody)) }) - mockMessenger.AssertExpectations(t) + mockMessageDispatcher.AssertExpectations(t) reCAPTCHAValidator.AssertExpectations(t) } diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 77296800c..e9f13e404 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -64,7 +64,7 @@ type ServeOptions struct { CorsAllowedOrigins []string authManager auth.AuthManager EmailMessengerClient message.MessengerClient - SMSMessengerClient message.MessengerClient + MessageDispatcher message.MessageDispatcherInterface SEP24JWTSecret string sep24JWTManager *anchorplatform.JWTManager BaseURL string @@ -471,7 +471,7 @@ func handleHTTP(o ServeOptions) *chi.Mux { sep24HeaderTokenAuthenticationMiddleware := anchorplatform.SEP24HeaderTokenAuthenticateMiddleware(o.sep24JWTManager, o.NetworkPassphrase, o.tenantManager, o.SingleTenantMode) r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/otp", httphandler.ReceiverSendOTPHandler{ Models: o.Models, - SMSMessengerClient: o.SMSMessengerClient, + MessageDispatcher: o.MessageDispatcher, ReCAPTCHAValidator: reCAPTCHAValidator, }.ServeHTTP) r.With(sep24HeaderTokenAuthenticationMiddleware).Post("/verification", httphandler.VerifyReceiverRegistrationHandler{ diff --git a/internal/serve/serve_test.go b/internal/serve/serve_test.go index 98b19b220..1ebcd39cb 100644 --- a/internal/serve/serve_test.go +++ b/internal/serve/serve_test.go @@ -304,6 +304,12 @@ func getServeOptionsForTests(t *testing.T, dbConnectionPool db.DBConnectionPool) messengerClientMock := message.MessengerClientMock{} messengerClientMock.On("SendMessage", mock.Anything).Return(nil) + messageDispatcherMock := message.NewMockMessageDispatcher(t) + messageDispatcherMock. + On("SendMessage", mock.Anything, mock.Anything). + Return(nil). + Maybe() + crashTrackerClient, err := crashtracker.NewDryRunClient() require.NoError(t, err) @@ -351,7 +357,7 @@ func getServeOptionsForTests(t *testing.T, dbConnectionPool db.DBConnectionPool) SEP24JWTSecret: "jwt_secret_1234567890", AnchorPlatformOutgoingJWTSecret: "jwt_secret_1234567890", AnchorPlatformBasePlatformURL: "https://test.com", - SMSMessengerClient: &messengerClientMock, + MessageDispatcher: messageDispatcherMock, Version: "x.y.z", NetworkPassphrase: network.TestNetworkPassphrase, SubmitterEngine: submitterEngine, diff --git a/internal/services/send_receiver_wallets_invite_service.go b/internal/services/send_receiver_wallets_invite_service.go index 85988ad91..6a7f342ae 100644 --- a/internal/services/send_receiver_wallets_invite_service.go +++ b/internal/services/send_receiver_wallets_invite_service.go @@ -27,7 +27,7 @@ type SendReceiverWalletInviteServiceInterface interface { } type SendReceiverWalletInviteService struct { - messengerClient message.MessengerClient + messageDispatcher message.MessageDispatcherInterface Models *data.Models maxInvitationSMSResendAttempts int64 sep10SigningPrivateKey string @@ -37,8 +37,8 @@ type SendReceiverWalletInviteService struct { var _ SendReceiverWalletInviteServiceInterface = new(SendReceiverWalletInviteService) func (s SendReceiverWalletInviteService) validate() error { - if s.messengerClient == nil { - return fmt.Errorf("messenger client can't be nil") + if s.messageDispatcher == nil { + return fmt.Errorf("messenger dispatcher can't be nil") } return nil @@ -139,7 +139,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive msgTemplate, err = template.New("").Parse(*disbursementSMSRegistrationMessageTemplate) if err != nil { - return fmt.Errorf("error parsing disbursement SMS registration message template: %w", err) + return fmt.Errorf("parsing disbursement SMS registration message template: %w", err) } } @@ -152,7 +152,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive RegistrationLink: template.HTML(registrationLink), }) if err != nil { - return fmt.Errorf("error executing registration message template: %w", err) + return fmt.Errorf("executing registration message template: %w", err) } msg := message.Message{ @@ -162,7 +162,15 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive assetID := rwa.Asset.ID receiverWalletID := rwa.ReceiverWallet.ID - messageType := s.messengerClient.MessengerType() + + // TODO: SDP-1316 - Update send and auto-retry invitation scheduler job to work with both SMS and email + messageChannel := message.MessageChannelSMS + messageClient, err := s.messageDispatcher.GetClient(messageChannel) + if err != nil { + return fmt.Errorf("getting message client: %w", err) + } + + messageType := messageClient.MessengerType() msgToInsert := &data.MessageInsert{ Type: messageType, AssetID: &assetID, @@ -174,7 +182,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive // We assume that the message will be sent at first msgToInsert.Status = data.SuccessMessageStatus - if err := s.messengerClient.SendMessage(msg); err != nil { + if err := s.messageDispatcher.SendMessage(msg, messageChannel); err != nil { msg := fmt.Sprintf( "error sending message to receiver ID %s for receiver wallet ID %s using messenger type %s", rwa.ReceiverWallet.Receiver.ID, rwa.ReceiverWallet.ID, messageType, @@ -284,9 +292,9 @@ func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Con return true } -func NewSendReceiverWalletInviteService(models *data.Models, messengerClient message.MessengerClient, sep10SigningPrivateKey string, maxInvitationSMSResendAttempts int64, crashTrackerClient crashtracker.CrashTrackerClient) (*SendReceiverWalletInviteService, error) { +func NewSendReceiverWalletInviteService(models *data.Models, messageDispatcher message.MessageDispatcherInterface, sep10SigningPrivateKey string, maxInvitationSMSResendAttempts int64, crashTrackerClient crashtracker.CrashTrackerClient) (*SendReceiverWalletInviteService, error) { s := &SendReceiverWalletInviteService{ - messengerClient: messengerClient, + messageDispatcher: messageDispatcher, Models: models, maxInvitationSMSResendAttempts: maxInvitationSMSResendAttempts, sep10SigningPrivateKey: sep10SigningPrivateKey, diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go index 9abbc4eb0..a9947c08d 100644 --- a/internal/services/send_receiver_wallets_invite_service_test.go +++ b/internal/services/send_receiver_wallets_invite_service_test.go @@ -63,11 +63,14 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { ctx := tenant.SaveTenantInContext(context.Background(), tenantInfo) stellarSecretKey := "SBUSPEKAZKLZSWHRSJ2HWDZUK6I3IVDUWA7JJZSGBLZ2WZIUJI7FPNB5" - messengerClientMock := &message.MessengerClientMock{} - messengerClientMock. + messageClientMock := message.NewMessengerClientMock(t) + messageClientMock. On("MessengerType"). - Return(message.MessengerTypeTwilioSMS). - Maybe() + Return(message.MessengerTypeTwilioSMS) + messageDispatcherMock := message.NewMockMessageDispatcher(t) + messageDispatcherMock. + On("GetClient", message.MessageChannelSMS). + Return(messageClientMock, nil) mockCrashTrackerClient := &crashtracker.MockCrashTrackerClient{} @@ -101,11 +104,11 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { t.Run("returns error when service has wrong setup", func(t *testing.T) { _, err := NewSendReceiverWalletInviteService(models, nil, stellarSecretKey, 3, mockCrashTrackerClient) - assert.EqualError(t, err, "invalid service setup: messenger client can't be nil") + assert.EqualError(t, err, "invalid service setup: messenger dispatcher can't be nil") }) t.Run("inserts the failed sent message", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -156,17 +159,17 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2) mockErr := errors.New("unexpected error") - messengerClientMock. + messageDispatcherMock. On("SendMessage", message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }). + }, message.MessageChannelSMS). Return(errors.New("unexpected error")). Once(). On("SendMessage", message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }). + }, message.MessageChannelSMS). Return(nil). Once() @@ -245,7 +248,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("send invite successfully", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -295,17 +298,17 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { require.NoError(t, err) contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2) - messengerClientMock. + messageDispatcherMock. On("SendMessage", message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }). + }, message.MessageChannelSMS). Return(nil). Once(). On("SendMessage", message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }). + }, message.MessageChannelSMS). Return(nil). Once() @@ -376,7 +379,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("send invite successfully with custom invite message", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -430,17 +433,17 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { require.NoError(t, err) contentWallet2 := fmt.Sprintf("%s %s", customInvitationMessage, deepLink2) - messengerClientMock. + messageDispatcherMock. On("SendMessage", message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }). + }, message.MessageChannelSMS). Return(nil). Once(). On("SendMessage", message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }). + }, message.MessageChannelSMS). Return(nil). Once() @@ -511,7 +514,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("doesn't resend the invitation SMS when organization's SMS Resend Interval is nil and the invitation was already sent", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -556,7 +559,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("doesn't resend the invitation SMS when receiver reached the maximum number of resend attempts", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -636,7 +639,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("doesn't resend invitation SMS when receiver is not in the resend period", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -683,7 +686,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { }) t.Run("successfully resend the invitation SMS", func(t *testing.T) { - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -723,11 +726,11 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { require.NoError(t, err) contentWallet1 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink1) - messengerClientMock. + messageDispatcherMock. On("SendMessage", message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }). + }, message.MessageChannelSMS). Return(nil). Once() @@ -791,7 +794,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { SMSRegistrationMessageTemplate: "SMS Registration Message template test disbursement 4:", }) - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -841,17 +844,17 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { require.NoError(t, err) contentDisbursement4 := fmt.Sprintf("%s %s", disbursement4.SMSRegistrationMessageTemplate, deepLink2) - messengerClientMock. + messageDispatcherMock. On("SendMessage", message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentDisbursement3, - }). + }, message.MessageChannelSMS). Return(nil). Once(). On("SendMessage", message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentDisbursement4, - }). + }, message.MessageChannelSMS). Return(nil). Once() @@ -930,7 +933,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { SMSRegistrationMessageTemplate: "SMS Registration Message template test disbursement:", }) - s, err := NewSendReceiverWalletInviteService(models, messengerClientMock, stellarSecretKey, 3, mockCrashTrackerClient) + s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) require.NoError(t, err) data.DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) @@ -970,11 +973,11 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { require.NoError(t, err) contentDisbursement := fmt.Sprintf("%s %s", disbursement.SMSRegistrationMessageTemplate, deepLink1) - messengerClientMock. + messageDispatcherMock. On("SendMessage", message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentDisbursement, - }). + }, message.MessageChannelSMS). Return(nil). Once() @@ -1021,7 +1024,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Nil(t, msg.AssetID) }) - messengerClientMock.AssertExpectations(t) + messageDispatcherMock.AssertExpectations(t) } func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) { From 662a1e17262d684b8d22d143d1090d346fc2bacb Mon Sep 17 00:00:00 2001 From: Erica Liu Date: Thu, 15 Aug 2024 16:10:09 -0700 Subject: [PATCH 10/75] [Chore] Add global singleTenantMode global value for SDP and dashboard (#392) --- dev/env-config.js | 3 ++- helmchart/sdp/Chart.yaml | 2 +- helmchart/sdp/README.md | 1 + helmchart/sdp/templates/01.1-configmap-sdp.yaml | 1 + helmchart/sdp/templates/01.4-configmap-dashboard.yaml | 5 +++-- helmchart/sdp/values.yaml | 3 +++ 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dev/env-config.js b/dev/env-config.js index 6b1a8dcbc..f74fc26ab 100644 --- a/dev/env-config.js +++ b/dev/env-config.js @@ -3,5 +3,6 @@ window._env_ = { STELLAR_EXPERT_URL: "https://stellar.expert/explorer/testnet", HORIZON_URL: "https://horizon-testnet.stellar.org", USDC_ASSET_ISSUER: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - RECAPTCHA_SITE_KEY: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" + RECAPTCHA_SITE_KEY: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI", + SINGLE_TENANT_MODE: false }; diff --git a/helmchart/sdp/Chart.yaml b/helmchart/sdp/Chart.yaml index 463527fd2..38c5197a1 100644 --- a/helmchart/sdp/Chart.yaml +++ b/helmchart/sdp/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: stellar-disbursement-platform description: A Helm chart for the Stellar Disbursement Platform Backend (A.K.A. `sdp`) -version: "2.1.0" +version: "2.1.1" appVersion: "2.1.0" type: application maintainers: diff --git a/helmchart/sdp/README.md b/helmchart/sdp/README.md index 85e57c306..31e3a866a 100644 --- a/helmchart/sdp/README.md +++ b/helmchart/sdp/README.md @@ -85,6 +85,7 @@ These parameters are shared by all charts. | `global.eventBroker.consumerGroupId` | The consumer group ID for the event broker. | `nil` | | `global.eventBroker.kafka` | Configuration related to the Kafka event broker. | | | `global.eventBroker.kafka.securityProtocol` | The security protocol to be used for the Kafka broker. Options: "PLAINTEXT", "SASL_SSL", "SASL_PLAINTEXT", "SSL". | `nil` | +| `global.singleTenantMode` | Determines if the SDP service is running in single-tenant mode. | `false` | ### Stellar Disbursement Platform (SDP) parameters diff --git a/helmchart/sdp/templates/01.1-configmap-sdp.yaml b/helmchart/sdp/templates/01.1-configmap-sdp.yaml index 3e9498fe4..bd469ec62 100644 --- a/helmchart/sdp/templates/01.1-configmap-sdp.yaml +++ b/helmchart/sdp/templates/01.1-configmap-sdp.yaml @@ -32,6 +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 }} {{- tpl (toYaml .Values.sdp.configMap.data | nindent 2) . }} {{- end }} diff --git a/helmchart/sdp/templates/01.4-configmap-dashboard.yaml b/helmchart/sdp/templates/01.4-configmap-dashboard.yaml index 7fd61c67d..539b1b80d 100644 --- a/helmchart/sdp/templates/01.4-configmap-dashboard.yaml +++ b/helmchart/sdp/templates/01.4-configmap-dashboard.yaml @@ -23,10 +23,11 @@ data: {{- end }} {{- if eq (include "isPubnet" .) "true" }} HORIZON_URL: "https://horizon.stellar.org", - STELLAR_EXPERT_URL: "https://stellar.expert/explorer/pubnet" + STELLAR_EXPERT_URL: "https://stellar.expert/explorer/pubnet", {{- else }} HORIZON_URL: "https://horizon-testnet.stellar.org", - STELLAR_EXPERT_URL: "https://stellar.expert/explorer/testnet" + STELLAR_EXPERT_URL: "https://stellar.expert/explorer/testnet", {{- end }} + SINGLE_TENANT_MODE: {{ .Values.global.singleTenantMode }} }; {{- end }} diff --git a/helmchart/sdp/values.yaml b/helmchart/sdp/values.yaml index 46b64fc8d..1315287b4 100644 --- a/helmchart/sdp/values.yaml +++ b/helmchart/sdp/values.yaml @@ -77,6 +77,9 @@ global: kafka: securityProtocol: #required + ## @param global.singleTenantMode Determines if the SDP service is running in single-tenant mode. + singleTenantMode: false + # =========================== START sdp =========================== ## @section Stellar Disbursement Platform (SDP) parameters ## @descriptionStart From 473febca775865b278700d8b24a5df514cb6b60f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:32:52 +0000 Subject: [PATCH 11/75] Bump docker/build-push-action in the all-actions group (#393) --- .github/workflows/docker_image_public_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_image_public_release.yml b/.github/workflows/docker_image_public_release.yml index 307125873..61e89daac 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.6.1 + uses: docker/build-push-action@v6.7.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.6.1 + uses: docker/build-push-action@v6.7.0 with: push: true build-args: | From 4659fa3e1d556ef1df1cd97e751c768d14676c77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:57:42 +0000 Subject: [PATCH 12/75] Bump golang in the all-docker group (#394) --- Dockerfile | 2 +- Dockerfile.development | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index af6e7a18e..95c2c2bd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # To push: # make docker-push -FROM golang:1.22.6-bullseye AS build +FROM golang:1.23.0-bullseye AS build ARG GIT_COMMIT WORKDIR /src/stellar-disbursement-platform diff --git a/Dockerfile.development b/Dockerfile.development index cc25b9004..7265e6bb4 100644 --- a/Dockerfile.development +++ b/Dockerfile.development @@ -1,5 +1,5 @@ # Stage 1: Build the Go application -FROM golang:1.22.6-bullseye AS build +FROM golang:1.23.0-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.22.6-bullseye AS development +FROM golang:1.23.0-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 2106f14b34f1371350d92d6ef9bfae387df3c91e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:21:42 +0000 Subject: [PATCH 13/75] Bump the minor-and-patch group across 1 directory with 3 updates (#395) --- go.list | 30 ++++++++++++++++-------------- go.mod | 21 +++++++++++---------- go.sum | 52 ++++++++++++++++++++++++++++------------------------ 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/go.list b/go.list index 5cceedf01..661078a70 100644 --- a/go.list +++ b/go.list @@ -69,7 +69,7 @@ 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 github.com/go-chi/chi/v5 v5.1.0 -github.com/go-chi/httprate v0.12.0 +github.com/go-chi/httprate v0.12.1 github.com/go-errors/errors v1.5.1 github.com/go-gorp/gorp/v3 v3.1.0 github.com/go-kit/log v0.2.1 @@ -144,11 +144,12 @@ github.com/kataras/iris/v12 v12.2.0 github.com/kataras/pio v0.0.11 github.com/kataras/sitemap v0.0.6 github.com/kataras/tunnel v0.0.4 -github.com/klauspost/compress v1.17.7 +github.com/klauspost/compress v1.17.9 github.com/kr/fs v0.1.0 github.com/kr/pretty v0.3.1 github.com/kr/pty v1.1.1 github.com/kr/text v0.2.0 +github.com/kylelemons/godebug v1.1.0 github.com/labstack/echo/v4 v4.10.0 github.com/labstack/gommon v0.4.0 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 @@ -177,6 +178,7 @@ github.com/mitchellh/reflectwalk v1.0.2 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd github.com/modern-go/reflect2 v1.0.2 github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f github.com/nats-io/nats.go v1.34.0 github.com/nats-io/nkeys v0.4.7 @@ -199,10 +201,10 @@ 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.19.1 -github.com/prometheus/client_model v0.5.0 -github.com/prometheus/common v0.48.0 -github.com/prometheus/procfs v0.12.0 +github.com/prometheus/client_golang v1.20.0 +github.com/prometheus/client_model v0.6.1 +github.com/prometheus/common v0.55.0 +github.com/prometheus/procfs v0.15.1 github.com/rivo/uniseg v0.2.0 github.com/rogpeppe/go-internal v1.11.0 github.com/rs/cors v1.11.0 @@ -270,15 +272,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.25.0 +golang.org/x/crypto v0.26.0 golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d golang.org/x/mod v0.17.0 -golang.org/x/net v0.24.0 -golang.org/x/oauth2 v0.20.0 -golang.org/x/sync v0.7.0 -golang.org/x/sys v0.22.0 -golang.org/x/term v0.22.0 -golang.org/x/text v0.16.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.23.0 +golang.org/x/term v0.23.0 +golang.org/x/text v0.17.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 @@ -288,7 +290,7 @@ google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 google.golang.org/grpc v1.63.2 -google.golang.org/protobuf v1.34.1 +google.golang.org/protobuf v1.34.2 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/djherbis/atime.v1 v1.0.0 gopkg.in/djherbis/stream.v1 v1.3.1 diff --git a/go.mod b/go.mod index f13996119..c7e9c522c 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/getsentry/sentry-go v0.28.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.12.0 + github.com/go-chi/httprate v0.12.1 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 @@ -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.19.1 + github.com/prometheus/client_golang v1.20.0 github.com/rs/cors v1.11.0 github.com/rubenv/sql-migrate v1.7.0 github.com/segmentio/kafka-go v0.4.47 @@ -29,7 +29,7 @@ require ( github.com/stellar/go v0.0.0-20240617183518-100dc4fa6043 github.com/stretchr/testify v1.9.0 github.com/twilio/twilio-go v1.22.3 - golang.org/x/crypto v0.25.0 + golang.org/x/crypto v0.26.0 golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d ) @@ -46,17 +46,18 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 // indirect @@ -70,9 +71,9 @@ 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.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.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 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a40832c56..d390a623a 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyN 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= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/httprate v0.12.0 h1:08D/te3pOTJe5+VAZTQrHxwdsH2NyliiUoRD1naKaMg= -github.com/go-chi/httprate v0.12.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= +github.com/go-chi/httprate v0.12.1 h1:55l3IWrPcipqKb72yBzH+grF51z5w+2Bb/Qmu1bos/E= +github.com/go-chi/httprate v0.12.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= @@ -86,8 +86,8 @@ github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -95,6 +95,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= @@ -111,6 +113,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ= github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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= @@ -134,14 +138,14 @@ 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.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI= +github.com/prometheus/client_golang v1.20.0/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= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= @@ -225,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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 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= @@ -240,14 +244,14 @@ 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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +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/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= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -262,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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.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= @@ -276,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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.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= @@ -286,8 +290,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 034777bbaf7c681780084af6e599d1004f9bb088 Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Tue, 20 Aug 2024 15:45:18 -0700 Subject: [PATCH 14/75] [SDP-1297] Add message channel priority to organizations table (#400) --- ...organizations-message-channel-priority.sql | 64 ++++++++ internal/data/organizations.go | 64 +++++++- internal/data/organizations_test.go | 96 ++++++++--- internal/message/aws_ses_client_test.go | 2 +- internal/message/message.go | 43 ++++- internal/message/message_dispatcher.go | 36 ++++- internal/message/message_dispatcher_test.go | 153 +++++++++++++++--- internal/message/message_test.go | 93 ++++++++++- .../mock_message_dispatcher_interface.go | 10 +- ...eceiver_wallets_sms_invitation_job_test.go | 9 +- internal/serve/httphandler/profile_handler.go | 1 + .../serve/httphandler/profile_handler_test.go | 18 ++- .../httphandler/receiver_send_otp_handler.go | 2 +- .../receiver_send_otp_handler_test.go | 22 ++- .../send_receiver_wallets_invite_service.go | 2 +- ...nd_receiver_wallets_invite_service_test.go | 41 ++--- 16 files changed, 545 insertions(+), 111 deletions(-) create mode 100644 db/migrations/sdp-migrations/2024-08-17.0-organizations-message-channel-priority.sql diff --git a/db/migrations/sdp-migrations/2024-08-17.0-organizations-message-channel-priority.sql b/db/migrations/sdp-migrations/2024-08-17.0-organizations-message-channel-priority.sql new file mode 100644 index 000000000..a14272342 --- /dev/null +++ b/db/migrations/sdp-migrations/2024-08-17.0-organizations-message-channel-priority.sql @@ -0,0 +1,64 @@ +-- This migration adds the message_channel type and the message_channel_priority column to the organizations table. +-- +migrate Up + +-- Create the message_channel enum type +CREATE TYPE message_channel AS ENUM ('SMS', 'EMAIL'); + +-- Add the message_channel_priority column to the organizations table +ALTER TABLE organizations + ADD COLUMN message_channel_priority message_channel[] NOT NULL + DEFAULT ARRAY['SMS'::message_channel, 'EMAIL'::message_channel]; + +-- Create a function to check if all message_channel values are included and valid +-- +migrate StatementBegin +CREATE OR REPLACE FUNCTION check_message_channel_priority() + RETURNS TRIGGER AS $$ +DECLARE + all_channels message_channel[]; + duplicate_channels message_channel[]; +BEGIN + -- Get all possible message_channel values + SELECT array_agg(enumlabel::message_channel) + INTO all_channels + FROM pg_enum + WHERE enumtypid = 'message_channel'::regtype; + + -- Check if all channels are included in the new value + IF NOT (SELECT all_channels <@ NEW.message_channel_priority) THEN + RAISE EXCEPTION 'message_channel_priority must include all possible message_channel values'; + END IF; + + -- Check for duplicates + SELECT ARRAY(SELECT channel + FROM unnest(NEW.message_channel_priority) channel + GROUP BY channel + HAVING COUNT(*) > 1) + INTO duplicate_channels; + + IF array_length(duplicate_channels, 1) > 0 THEN + RAISE EXCEPTION 'message_channel_priority must not contain duplicate values: %', duplicate_channels; + END IF; + + RETURN NEW; +END; +$$ language 'plpgsql'; +-- +migrate StatementEnd + +-- Create the trigger +CREATE TRIGGER validate_organizations_message_channel_priority + BEFORE INSERT OR UPDATE OF message_channel_priority ON organizations + FOR EACH ROW EXECUTE FUNCTION check_message_channel_priority(); + +-- +migrate Down + +-- Drop the trigger +DROP TRIGGER IF EXISTS validate_organizations_message_channel_priority ON organizations; + +-- Drop the function +DROP FUNCTION IF EXISTS check_message_channel_priority(); + +-- Remove the message_channel_priority column +ALTER TABLE organizations DROP COLUMN IF EXISTS message_channel_priority; + +-- Remove the message_channel enum type +DROP TYPE IF EXISTS message_channel; \ No newline at end of file diff --git a/internal/data/organizations.go b/internal/data/organizations.go index 660c04219..35f0a477a 100644 --- a/internal/data/organizations.go +++ b/internal/data/organizations.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "database/sql" + "database/sql/driver" "errors" "fmt" "image" @@ -20,6 +21,7 @@ import ( _ "image/png" "github.com/stellar/stellar-disbursement-platform-backend/db" + "github.com/stellar/stellar-disbursement-platform-backend/internal/message" ) const ( @@ -42,12 +44,13 @@ type Organization struct { // When the {{.OTP}} is not found in the message, it's added at the beginning of the message. // Example: // {{.OTP}} OTPMessageTemplate - OTPMessageTemplate string `json:"otp_message_template" db:"otp_message_template"` - PrivacyPolicyLink *string `json:"privacy_policy_link" db:"privacy_policy_link"` - Logo []byte `db:"logo"` - IsApprovalRequired bool `json:"is_approval_required" db:"is_approval_required"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + OTPMessageTemplate string `json:"otp_message_template" db:"otp_message_template"` + PrivacyPolicyLink *string `json:"privacy_policy_link" db:"privacy_policy_link"` + Logo []byte `db:"logo"` + IsApprovalRequired bool `json:"is_approval_required" db:"is_approval_required"` + MessageChannelPriority MessageChannelPriority `json:"message_channel_priority" db:"message_channel_priority"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } type OrganizationUpdate struct { @@ -252,3 +255,52 @@ func (om *OrganizationModel) Update(ctx context.Context, ou *OrganizationUpdate) return nil } + +type MessageChannelPriority []message.MessageChannel + +var DefaultMessageChannelPriority = MessageChannelPriority{ + message.MessageChannelSMS, + message.MessageChannelEmail, +} + +func (mcp *MessageChannelPriority) Scan(src interface{}) error { + if src == nil { + *mcp = nil + return nil + } + + byteValue, ok := src.([]byte) + if !ok { + return fmt.Errorf("unexpected type for MessageChannelPriority %T", src) + } + + // Convert []byte to string and remove the curly braces. E.g. `{SMS,EMAIL}` + strValue := strings.Trim(string(byteValue), "{}") + + // Split the string into individual channel values. E.g. `SMS,EMAIL` -> ["SMS", "EMAIL"] + channels := strings.Split(strValue, ",") + + *mcp = make(MessageChannelPriority, len(channels)) + for i, ch := range channels { + (*mcp)[i] = message.MessageChannel(strings.TrimSpace(ch)) + } + + return nil +} + +var _ sql.Scanner = (*MessageChannelPriority)(nil) + +func (mcp MessageChannelPriority) Value() (driver.Value, error) { + if len(mcp) == 0 { + return "{}", nil + } + + channels := make([]string, len(mcp)) + for i, ch := range mcp { + channels[i] = string(ch) + } + + return "{" + strings.Join(channels, ",") + "}", nil +} + +var _ driver.Valuer = MessageChannelPriority{} diff --git a/internal/data/organizations_test.go b/internal/data/organizations_test.go index b6c41f6be..2f0124ecc 100644 --- a/internal/data/organizations_test.go +++ b/internal/data/organizations_test.go @@ -14,6 +14,7 @@ import ( "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/message" ) func Test_Organizations_DatabaseTriggers(t *testing.T) { @@ -75,6 +76,7 @@ func Test_Organizations_Get(t *testing.T) { assert.NotEmpty(t, gotOrganization.UpdatedAt) assert.False(t, gotOrganization.IsApprovalRequired) assert.Nil(t, gotOrganization.PrivacyPolicyLink) + assert.Equal(t, MessageChannelPriority{"SMS", "EMAIL"}, gotOrganization.MessageChannelPriority) }) } @@ -194,27 +196,18 @@ func Test_Organizations_Update(t *testing.T) { ctx := context.Background() - resetOrganizationInfo := func(t *testing.T, ctx context.Context) { - const q = ` - UPDATE - organizations - SET - name = 'MyCustomAid', logo = NULL, timezone_utc_offset = '+00:00', - sms_registration_message_template = DEFAULT, otp_message_template = DEFAULT` - _, err := dbConnectionPool.ExecContext(ctx, q) - require.NoError(t, err) - } - organizationModel := &OrganizationModel{dbConnectionPool: dbConnectionPool} t.Run("returns error with invalid OrganizationUpdate", func(t *testing.T) { + defer resetOrganizationInfo(t, ctx, dbConnectionPool) + ou := &OrganizationUpdate{} err := organizationModel.Update(ctx, ou) assert.EqualError(t, err, "invalid organization update: name, timezone UTC offset, approval workflow flag, SMS Resend Interval, SMS invite template, OTP message template, privacy policy link or logo is required") }) t.Run("updates only organization's name successfully", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -237,7 +230,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates only organization's timezone UTC offset successfully", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -260,7 +253,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates only organization's logo successfully", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -286,7 +279,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates only organization's is_approval_required successfully", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -304,7 +297,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates organization's name, timezone UTC offset and logo successfully", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -332,7 +325,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates the organization's SMSRegistrationMessageTemplate", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) defaultMessage := "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register." o, err := organizationModel.Get(ctx) @@ -369,7 +362,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates the organization's OTPMessageTemplate", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) defaultMessage := "{{.OTP}} is your {{.OrganizationName}} phone verification code." o, err := organizationModel.Get(ctx) @@ -406,7 +399,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates the organization's SMSResendInterval", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -431,7 +424,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates the organization's PaymentCancellationPeriod", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -456,7 +449,7 @@ func Test_Organizations_Update(t *testing.T) { }) t.Run("updates the organization's PrivacyPolicyLink", func(t *testing.T) { - resetOrganizationInfo(t, ctx) + defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) @@ -480,3 +473,64 @@ func Test_Organizations_Update(t *testing.T) { assert.Nil(t, o.PrivacyPolicyLink) }) } + +func TestOrganizationModel_UpdateMessageChannelPriority(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() + organizationModel := &OrganizationModel{dbConnectionPool: dbConnectionPool} + + t.Run("succeeds when all channels are included", func(t *testing.T) { + defer resetOrganizationInfo(t, ctx, dbConnectionPool) + + _, err := dbConnectionPool.ExecContext(ctx, "UPDATE organizations SET message_channel_priority = $1", DefaultMessageChannelPriority) + require.NoError(t, err) + + // Verify the update + org, err := organizationModel.Get(ctx) + require.NoError(t, err) + assert.Equal(t, MessageChannelPriority{message.MessageChannelSMS, message.MessageChannelEmail}, org.MessageChannelPriority) + }) + + t.Run("fails when not all channels are included", func(t *testing.T) { + defer resetOrganizationInfo(t, ctx, dbConnectionPool) + + _, err := dbConnectionPool.ExecContext(ctx, "UPDATE organizations SET message_channel_priority = $1", MessageChannelPriority{"SMS"}) + require.Error(t, err) + assert.ErrorContains(t, err, "message_channel_priority must include all possible message_channel values") + }) + + t.Run("fails when duplicate channels are included", func(t *testing.T) { + defer resetOrganizationInfo(t, ctx, dbConnectionPool) + + _, err := dbConnectionPool.ExecContext(ctx, "UPDATE organizations SET message_channel_priority = $1", MessageChannelPriority{"SMS", "EMAIL", "SMS"}) + require.Error(t, err) + assert.ErrorContains(t, err, "message_channel_priority must not contain duplicate values: {SMS}") + }) + + t.Run("fails when invalid channel is included", func(t *testing.T) { + defer resetOrganizationInfo(t, ctx, dbConnectionPool) + + _, err := dbConnectionPool.ExecContext(ctx, "UPDATE organizations SET message_channel_priority = $1", MessageChannelPriority{"SMS", "WHATSAPP"}) + require.Error(t, err) + assert.ErrorContains(t, err, "invalid input value for enum message_channel: \"WHATSAPP\"") + }) +} + +func resetOrganizationInfo(t *testing.T, ctx context.Context, dbConnectionPool db.DBConnectionPool) { + t.Helper() + + const q = ` + UPDATE + organizations + SET + name = 'MyCustomAid', logo = NULL, timezone_utc_offset = '+00:00', + sms_registration_message_template = DEFAULT, otp_message_template = DEFAULT, message_channel_priority = '{"SMS", "EMAIL"}'` + _, err := dbConnectionPool.ExecContext(ctx, q) + require.NoError(t, err) +} diff --git a/internal/message/aws_ses_client_test.go b/internal/message/aws_ses_client_test.go index 8b708c54a..6e8a88708 100644 --- a/internal/message/aws_ses_client_test.go +++ b/internal/message/aws_ses_client_test.go @@ -57,7 +57,7 @@ func Test_NewAWSSESClient(t *testing.T) { func Test_AWSSES_SendMessage_messageIsInvalid(t *testing.T) { var mAWS MessengerClient = &awsSESClient{} err := mAWS.SendMessage(Message{}) - require.EqualError(t, err, "validating message to send an email through AWS: invalid message: email cannot be empty") + require.EqualError(t, err, "validating message to send an email through AWS: invalid e-mail: invalid email format: email cannot be empty") } func Test_AWSSES_SendMessage_errorIsHandledCorrectly(t *testing.T) { diff --git a/internal/message/message.go b/internal/message/message.go index 265d2f040..7dbab1533 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -15,7 +15,7 @@ type Message struct { } // ValidateFor validates if the message object is valid for the given messengerType. -func (s *Message) ValidateFor(messengerType MessengerType) error { +func (s Message) ValidateFor(messengerType MessengerType) error { if messengerType.IsSMS() { if err := utils.ValidatePhoneNumber(s.ToPhoneNumber); err != nil { return fmt.Errorf("invalid message: %w", err) @@ -23,12 +23,8 @@ func (s *Message) ValidateFor(messengerType MessengerType) error { } if messengerType.IsEmail() { - if err := utils.ValidateEmail(s.ToEmail); err != nil { - return fmt.Errorf("invalid message: %w", err) - } - - if strings.Trim(s.Title, " ") == "" { - return fmt.Errorf("title is empty") + if err := s.IsValidForEmail(); err != nil { + return fmt.Errorf("invalid e-mail: %w", err) } } @@ -38,3 +34,36 @@ func (s *Message) ValidateFor(messengerType MessengerType) error { return nil } + +func (s Message) IsValidForEmail() error { + if err := utils.ValidateEmail(s.ToEmail); err != nil { + return fmt.Errorf("invalid email format: %w", err) + } + + if strings.TrimSpace(s.Title) == "" { + return fmt.Errorf("title is empty") + } + return nil +} + +func (s Message) SupportedChannels() []MessageChannel { + var supportedChannels []MessageChannel + + if utils.ValidatePhoneNumber(s.ToPhoneNumber) == nil { + supportedChannels = append(supportedChannels, MessageChannelSMS) + } + + if err := s.IsValidForEmail(); err == nil { + supportedChannels = append(supportedChannels, MessageChannelEmail) + } + + return supportedChannels +} + +func (s Message) String() string { + return fmt.Sprintf("Message{ToPhoneNumber: %s, ToEmail: %s, Message: %s, Title: %s}", + utils.TruncateString(s.ToPhoneNumber, 3), + utils.TruncateString(s.ToEmail, 3), + utils.TruncateString(s.Message, 3), + utils.TruncateString(s.Title, 3)) +} diff --git a/internal/message/message_dispatcher.go b/internal/message/message_dispatcher.go index 582e00745..c0c142133 100644 --- a/internal/message/message_dispatcher.go +++ b/internal/message/message_dispatcher.go @@ -17,7 +17,7 @@ const ( //go:generate mockery --name MessageDispatcherInterface --case=underscore --structname=MockMessageDispatcher --inpackage type MessageDispatcherInterface interface { RegisterClient(ctx context.Context, channel MessageChannel, client MessengerClient) - SendMessage(message Message, channel MessageChannel) error + SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error GetClient(channel MessageChannel) (MessengerClient, error) } @@ -36,13 +36,37 @@ func (d *MessageDispatcher) RegisterClient(ctx context.Context, channel MessageC d.clients[channel] = client } -func (d *MessageDispatcher) SendMessage(message Message, channel MessageChannel) error { - client, err := d.GetClient(channel) - if err != nil { - return fmt.Errorf("getting client for channel: %w", err) +func (d *MessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error { + supportedChannels := make(map[MessageChannel]bool) + for _, ch := range message.SupportedChannels() { + supportedChannels[ch] = true } - return client.SendMessage(message) + if len(supportedChannels) == 0 { + return fmt.Errorf("no valid channel found for message %s", message) + } + + for _, channel := range channelPriority { + if !supportedChannels[channel] { + log.Ctx(ctx).Debugf("Channel %q is not supported for the message %s", channel, message) + continue + } + + client, ok := d.clients[channel] + if !ok { + log.Ctx(ctx).Warnf("No client registered for channel %q", channel) + continue + } + + err := client.SendMessage(message) + if err == nil { + return nil + } + + log.Ctx(ctx).Errorf("Error sending message %s using channel %q: %v", message, channel, err) + } + + return fmt.Errorf("unable to send message %s using any of the supported channels [%v]", message, supportedChannels) } func (d *MessageDispatcher) GetClient(channel MessageChannel) (MessengerClient, error) { diff --git a/internal/message/message_dispatcher_test.go b/internal/message/message_dispatcher_test.go index e53637dec..bb62c7fe5 100644 --- a/internal/message/message_dispatcher_test.go +++ b/internal/message/message_dispatcher_test.go @@ -3,6 +3,7 @@ package message import ( "context" "errors" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -68,50 +69,152 @@ func Test_MessageDispatcher_GetClient(t *testing.T) { } func Test_MessageDispatcher_SendMessage(t *testing.T) { - ctx := context.Background() + emailMessage := Message{ + Title: "Test Title", + ToEmail: "mymail@stellar.org", + Message: "Test Message", + } - dispatcher := NewMessageDispatcher() - message := Message{} + smsMessage := Message{ + ToPhoneNumber: "+14152111111", + Message: "Test Message", + } + + multiChannelMessage := Message{ + Title: "Test Title", + ToEmail: "mymail@stellar.org", + ToPhoneNumber: "+14152111111", + Message: "Test Message", + } + + emptyMessage := Message{} tests := []struct { - name string - channel MessageChannel - setupMock func(*MessengerClientMock) - expectedErr error + name string + message Message + channelPriority []MessageChannel + supportedChannels []MessageChannel + setupMock func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) + expectedErr error }{ { - name: "Successful send", - channel: MessageChannelEmail, - setupMock: func(clientMock *MessengerClientMock) { - clientMock.On("SendMessage", message).Return(nil) + name: "fail when no supported channels", + message: emptyMessage, + channelPriority: []MessageChannel{MessageChannelEmail, MessageChannelSMS}, + supportedChannels: []MessageChannel{}, + setupMock: func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) {}, + expectedErr: fmt.Errorf("no valid channel found for message %s", emptyMessage), + }, + { + name: "fail when message with wrong format", + message: emailMessage, + channelPriority: []MessageChannel{MessageChannelSMS}, + supportedChannels: []MessageChannel{MessageChannelSMS}, + setupMock: func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) { + smsClientMock.AssertNotCalled(t, "SendMessage", emailMessage) + emailClientMock.AssertNotCalled(t, "SendMessage", emailMessage) + }, + expectedErr: fmt.Errorf("unable to send message %s using any of the supported channels [%v]", emailMessage, map[MessageChannel]bool{MessageChannelEmail: true}), + }, + { + name: "successful when single supported channel (e-mail)", + message: emailMessage, + channelPriority: []MessageChannel{MessageChannelEmail, MessageChannelSMS}, + supportedChannels: []MessageChannel{MessageChannelEmail}, + setupMock: func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) { + emailClientMock. + On("SendMessage", emailMessage). + Return(nil). + Once() + + smsClientMock.AssertNotCalled(t, "SendMessage", emailMessage) }, expectedErr: nil, }, { - name: "Client not found", - channel: MessageChannelSMS, - setupMock: func(clientMock *MessengerClientMock) {}, - expectedErr: errors.New("getting client for channel: no client registered for channel \"SMS\""), + name: "successful when single supported channel (sms)", + message: smsMessage, + channelPriority: []MessageChannel{MessageChannelEmail, MessageChannelSMS}, + supportedChannels: []MessageChannel{MessageChannelSMS}, + setupMock: func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) { + smsClientMock. + On("SendMessage", smsMessage). + Return(nil). + Once() + + emailClientMock.AssertNotCalled(t, "SendMessage", smsMessage) + }, + expectedErr: nil, + }, + { + name: "successful when multiple supported channels", + message: multiChannelMessage, + channelPriority: []MessageChannel{MessageChannelSMS, MessageChannelEmail}, + supportedChannels: []MessageChannel{MessageChannelEmail, MessageChannelSMS}, + setupMock: func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) { + smsClientMock. + On("SendMessage", multiChannelMessage). + Return(nil). + Once() + + emailClientMock.AssertNotCalled(t, "SendMessage", multiChannelMessage) + }, + expectedErr: nil, }, { - name: "Client error", - channel: MessageChannelEmail, - setupMock: func(clientMock *MessengerClientMock) { - clientMock.On("SendMessage", message).Return(errors.New("send error")) + name: "successful when first channel fails (sms) but second succeeds (e-mail)", + message: multiChannelMessage, + channelPriority: []MessageChannel{MessageChannelSMS, MessageChannelEmail}, + supportedChannels: []MessageChannel{MessageChannelSMS, MessageChannelEmail}, + setupMock: func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) { + smsClientMock. + On("SendMessage", multiChannelMessage). + Return(errors.New("send error")). + Once() + + emailClientMock. + On("SendMessage", multiChannelMessage). + Return(nil). + Once() }, - expectedErr: errors.New("send error"), + expectedErr: nil, + }, + { + name: "fail when all channels fail", + message: multiChannelMessage, + channelPriority: []MessageChannel{MessageChannelSMS, MessageChannelEmail}, + supportedChannels: []MessageChannel{MessageChannelSMS, MessageChannelEmail}, + setupMock: func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) { + emailClientMock. + On("SendMessage", multiChannelMessage). + Return(errors.New("send error")). + Once() + + smsClientMock. + On("SendMessage", multiChannelMessage). + Return(errors.New("send error")). + Once() + }, + expectedErr: fmt.Errorf("unable to send message %s using any of the supported channels [%v]", multiChannelMessage, map[MessageChannel]bool{MessageChannelEmail: true, MessageChannelSMS: true}), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := NewMessengerClientMock(t) - client.On("MessengerType").Return(MessengerTypeDryRun).Once() - dispatcher.RegisterClient(ctx, MessageChannelEmail, client) + ctx := context.Background() + dispatcher := NewMessageDispatcher() + + emailClient := NewMessengerClientMock(t) + emailClient.On("MessengerType").Return(MessengerTypeDryRun).Once() + dispatcher.RegisterClient(ctx, MessageChannelEmail, emailClient) + + smsClient := NewMessengerClientMock(t) + smsClient.On("MessengerType").Return(MessengerTypeDryRun).Once() + dispatcher.RegisterClient(ctx, MessageChannelSMS, smsClient) - tt.setupMock(client) + tt.setupMock(emailClient, smsClient) - err := dispatcher.SendMessage(message, tt.channel) + err := dispatcher.SendMessage(ctx, tt.message, tt.channelPriority) if tt.expectedErr != nil { assert.EqualError(t, err, tt.expectedErr.Error()) } else { diff --git a/internal/message/message_test.go b/internal/message/message_test.go index 42ea6633c..8ff596a98 100644 --- a/internal/message/message_test.go +++ b/internal/message/message_test.go @@ -50,19 +50,19 @@ func Test_message_Validate(t *testing.T) { name: "Email types need a non-empty email address", messengerType: MessengerTypeAWSEmail, message: Message{}, - wantErr: fmt.Errorf("invalid message: email cannot be empty"), + wantErr: fmt.Errorf("invalid e-mail: invalid email format: email cannot be empty"), }, { name: "Email types need a valid email address", messengerType: MessengerTypeAWSEmail, message: Message{ToEmail: "invalid-email"}, - wantErr: fmt.Errorf("invalid message: the provided email is not valid"), + wantErr: fmt.Errorf("invalid e-mail: invalid email format: the provided email is not valid"), }, { name: "Email types need a title", messengerType: MessengerTypeAWSEmail, message: Message{ToEmail: "foo@test.com", Title: " "}, - wantErr: fmt.Errorf("title is empty"), + wantErr: fmt.Errorf("invalid e-mail: title is empty"), }, { name: "[sms] message cannot be empty", @@ -89,3 +89,90 @@ func Test_message_Validate(t *testing.T) { }) } } + +func TestMessage_SupportedChannels(t *testing.T) { + testCases := []struct { + name string + message Message + wantChannels []MessageChannel + }{ + { + name: "sms only", + message: Message{ToPhoneNumber: "+14152111111", Message: "Hello"}, + wantChannels: []MessageChannel{MessageChannelSMS}, + }, + { + name: "e-mail only", + message: Message{ToEmail: "test@example.com", Title: "Test", Message: "Hello"}, + wantChannels: []MessageChannel{MessageChannelEmail}, + }, + { + name: "both sms and e-mail", + message: Message{ToPhoneNumber: "+14152111111", ToEmail: "test@example.com", Title: "Test", Message: "Hello"}, + wantChannels: []MessageChannel{MessageChannelSMS, MessageChannelEmail}, + }, + { + name: "neither sms nor e-mail", + message: Message{Message: "Hello"}, + wantChannels: []MessageChannel{}, + }, + { + name: "invalid phone number", + message: Message{ToPhoneNumber: "invalid", ToEmail: "test@example.com", Title: "Test", Message: "Hello"}, + wantChannels: []MessageChannel{MessageChannelEmail}, + }, + { + name: "invalid email", + message: Message{ToPhoneNumber: "+14152111111", ToEmail: "invalid", Title: "Test", Message: "Hello"}, + wantChannels: []MessageChannel{MessageChannelSMS}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotChannels := tc.message.SupportedChannels() + require.ElementsMatch(t, tc.wantChannels, gotChannels) + }) + } +} + +func TestMessage_String(t *testing.T) { + testCases := []struct { + name string + message Message + wantRepresentation string + }{ + { + name: "all fields present", + message: Message{ToPhoneNumber: "+14152111111", ToEmail: "test@example.com", Title: "Test Title", Message: "Hello, World!"}, + wantRepresentation: "Message{ToPhoneNumber: +14...111, ToEmail: tes...com, Message: Hel...ld!, Title: Tes...tle}", + }, + { + name: "only phone number", + message: Message{ToPhoneNumber: "+14152111111", Message: "Hello"}, + wantRepresentation: "Message{ToPhoneNumber: +14...111, ToEmail: , Message: Hello, Title: }", + }, + { + name: "only email", + message: Message{ToEmail: "test@example.com", Title: "Test", Message: "Hello"}, + wantRepresentation: "Message{ToPhoneNumber: , ToEmail: tes...com, Message: Hello, Title: Test}", + }, + { + name: "empty message", + message: Message{}, + wantRepresentation: "Message{ToPhoneNumber: , ToEmail: , Message: , Title: }", + }, + { + name: "long fields", + message: Message{ToPhoneNumber: "+14152111111", ToEmail: "very.long.email@example.com", Title: "This is a very long title", Message: "This is a very long message that should be truncated"}, + wantRepresentation: "Message{ToPhoneNumber: +14...111, ToEmail: ver...com, Message: Thi...ted, Title: Thi...tle}", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotRepresentation := tc.message.String() + require.Equal(t, tc.wantRepresentation, gotRepresentation) + }) + } +} diff --git a/internal/message/mock_message_dispatcher_interface.go b/internal/message/mock_message_dispatcher_interface.go index 763371f31..aceda8185 100644 --- a/internal/message/mock_message_dispatcher_interface.go +++ b/internal/message/mock_message_dispatcher_interface.go @@ -48,17 +48,17 @@ func (_m *MockMessageDispatcher) RegisterClient(ctx context.Context, channel Mes _m.Called(ctx, channel, client) } -// SendMessage provides a mock function with given fields: message, channel -func (_m *MockMessageDispatcher) SendMessage(message Message, channel MessageChannel) error { - ret := _m.Called(message, channel) +// SendMessage provides a mock function with given fields: ctx, message, channelPriority +func (_m *MockMessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error { + ret := _m.Called(ctx, message, channelPriority) if len(ret) == 0 { panic("no return value specified for SendMessage") } var r0 error - if rf, ok := ret.Get(0).(func(Message, MessageChannel) error); ok { - r0 = rf(message, channel) + if rf, ok := ret.Get(0).(func(context.Context, Message, []MessageChannel) error); ok { + r0 = rf(ctx, message, channelPriority) } else { r0 = ret.Error(0) } 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 38d853635..408ee2704 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 @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stellar/stellar-disbursement-platform-backend/db" @@ -233,16 +234,16 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { On("GetClient", message.MessageChannelSMS). Return(messengerClientMock, nil). Twice(). - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(mockErr). Once(). - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once() messengerClientMock. diff --git a/internal/serve/httphandler/profile_handler.go b/internal/serve/httphandler/profile_handler.go index e9d054817..7dbf0647d 100644 --- a/internal/serve/httphandler/profile_handler.go +++ b/internal/serve/httphandler/profile_handler.go @@ -367,6 +367,7 @@ func (h ProfileHandler) GetOrganizationInfo(rw http.ResponseWriter, req *http.Re "sms_resend_interval": 0, "payment_cancellation_period_days": 0, "privacy_policy_link": org.PrivacyPolicyLink, + "message_channel_priority": org.MessageChannelPriority, } if org.SMSRegistrationMessageTemplate != data.DefaultSMSRegistrationMessageTemplate { diff --git a/internal/serve/httphandler/profile_handler_test.go b/internal/serve/httphandler/profile_handler_test.go index a38b31621..93bd08f9f 100644 --- a/internal/serve/httphandler/profile_handler_test.go +++ b/internal/serve/httphandler/profile_handler_test.go @@ -1135,7 +1135,8 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "is_approval_required": false, "privacy_policy_link": null, "sms_resend_interval": 0, - "payment_cancellation_period_days": 0 + "payment_cancellation_period_days": 0, + "message_channel_priority": ["SMS", "EMAIL"] } `, newDistAccountJSON(t, defaultTenantDistAcc), defaultTenantDistAcc) @@ -1173,7 +1174,8 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "sms_registration_message_template": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", "sms_resend_interval": 0, "payment_cancellation_period_days": 0, - "privacy_policy_link": null + "privacy_policy_link": null, + "message_channel_priority": ["SMS", "EMAIL"] } `, newDistAccountJSON(t, defaultTenantDistAcc), defaultTenantDistAcc) @@ -1208,7 +1210,8 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "otp_message_template": "Here's your OTP Code to complete your registration. MyOrg ๐Ÿ‘‹", "sms_resend_interval": 0, "payment_cancellation_period_days": 0, - "privacy_policy_link": null + "privacy_policy_link": null, + "message_channel_priority": ["SMS", "EMAIL"] } `, newDistAccountJSON(t, defaultTenantDistAcc), defaultTenantDistAcc) @@ -1247,7 +1250,8 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "is_approval_required":false, "sms_resend_interval": 2, "payment_cancellation_period_days": 0, - "privacy_policy_link": null + "privacy_policy_link": null, + "message_channel_priority": ["SMS", "EMAIL"] } `, newDistAccountJSON(t, defaultTenantDistAcc), defaultTenantDistAcc) @@ -1286,7 +1290,8 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "is_approval_required":false, "sms_resend_interval": 0, "payment_cancellation_period_days": 5, - "privacy_policy_link": null + "privacy_policy_link": null, + "message_channel_priority": ["SMS", "EMAIL"] } `, newDistAccountJSON(t, defaultTenantDistAcc), defaultTenantDistAcc) @@ -1325,7 +1330,8 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "is_approval_required":false, "sms_resend_interval": 0, "payment_cancellation_period_days": 0, - "privacy_policy_link": "https://example.com/privacy-policy" + "privacy_policy_link": "https://example.com/privacy-policy", + "message_channel_priority": ["SMS", "EMAIL"] } `, newDistAccountJSON(t, defaultTenantDistAcc), defaultTenantDistAcc) diff --git a/internal/serve/httphandler/receiver_send_otp_handler.go b/internal/serve/httphandler/receiver_send_otp_handler.go index 303edc460..7e08ee3ef 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler.go +++ b/internal/serve/httphandler/receiver_send_otp_handler.go @@ -158,7 +158,7 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request // TODO: SDP-1296 - support multiple channels for OTP log.Ctx(ctx).Infof("sending OTP message to phone number: %s", truncatedPhoneNumber) - err = h.MessageDispatcher.SendMessage(msg, message.MessageChannelSMS) + err = h.MessageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority) if err != nil { httperror.InternalError(ctx, "Cannot send OTP message", err, nil).Render(w) return diff --git a/internal/serve/httphandler/receiver_send_otp_handler_test.go b/internal/serve/httphandler/receiver_send_otp_handler_test.go index 35138bd3e..1d3e7baf8 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler_test.go +++ b/internal/serve/httphandler/receiver_send_otp_handler_test.go @@ -168,11 +168,15 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { } req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - mockMessageDispatcher.On("SendMessage", mock.AnythingOfType("message.Message"), message.MessageChannelSMS). + mockMessageDispatcher. + On("SendMessage", + mock.Anything, + mock.AnythingOfType("message.Message"), + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). Run(func(args mock.Arguments) { - msg := args.Get(0).(message.Message) + msg := args.Get(1).(message.Message) assert.Contains(t, msg.Message, "is your MyCustomAid phone verification code.") assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message) }) @@ -212,11 +216,15 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { err = models.Organizations.Update(ctx, &data.OrganizationUpdate{OTPMessageTemplate: &customOTPMessage}) require.NoError(t, err) - mockMessageDispatcher.On("SendMessage", mock.AnythingOfType("message.Message"), message.MessageChannelSMS). + mockMessageDispatcher. + On("SendMessage", + mock.Anything, + mock.AnythingOfType("message.Message"), + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). Run(func(args mock.Arguments) { - msg := args.Get(0).(message.Message) + msg := args.Get(1).(message.Message) assert.Contains(t, msg.Message, customOTPMessage) assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message) }) @@ -251,7 +259,11 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { } req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - mockMessageDispatcher.On("SendMessage", mock.AnythingOfType("message.Message"), message.MessageChannelSMS). + mockMessageDispatcher. + On("SendMessage", + mock.Anything, + mock.AnythingOfType("message.Message"), + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(errors.New("error sending message")). Once() diff --git a/internal/services/send_receiver_wallets_invite_service.go b/internal/services/send_receiver_wallets_invite_service.go index 6a7f342ae..8a745016f 100644 --- a/internal/services/send_receiver_wallets_invite_service.go +++ b/internal/services/send_receiver_wallets_invite_service.go @@ -182,7 +182,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive // We assume that the message will be sent at first msgToInsert.Status = data.SuccessMessageStatus - if err := s.messageDispatcher.SendMessage(msg, messageChannel); err != nil { + if err := s.messageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority); err != nil { msg := fmt.Sprintf( "error sending message to receiver ID %s for receiver wallet ID %s using messenger type %s", rwa.ReceiverWallet.Receiver.ID, rwa.ReceiverWallet.ID, messageType, diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go index a9947c08d..cf5a32295 100644 --- a/internal/services/send_receiver_wallets_invite_service_test.go +++ b/internal/services/send_receiver_wallets_invite_service_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/stellar/go/support/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stellar/stellar-disbursement-platform-backend/db" @@ -160,16 +161,16 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { mockErr := errors.New("unexpected error") messageDispatcherMock. - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(errors.New("unexpected error")). Once(). - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once() @@ -299,16 +300,16 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2) messageDispatcherMock. - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once() @@ -434,16 +435,16 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { contentWallet2 := fmt.Sprintf("%s %s", customInvitationMessage, deepLink2) messageDispatcherMock. - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentWallet2, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once() @@ -727,10 +728,10 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { contentWallet1 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink1) messageDispatcherMock. - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentWallet1, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once() @@ -845,16 +846,16 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { contentDisbursement4 := fmt.Sprintf("%s %s", disbursement4.SMSRegistrationMessageTemplate, deepLink2) messageDispatcherMock. - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentDisbursement3, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, Message: contentDisbursement4, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once() @@ -974,10 +975,10 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { contentDisbursement := fmt.Sprintf("%s %s", disbursement.SMSRegistrationMessageTemplate, deepLink1) messageDispatcherMock. - On("SendMessage", message.Message{ + On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, Message: contentDisbursement, - }, message.MessageChannelSMS). + }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once() From 38e64e93f2030f1c774174cd903f26af1e326eb4 Mon Sep 17 00:00:00 2001 From: Erica Liu Date: Wed, 21 Aug 2024 11:52:39 -0700 Subject: [PATCH 15/75] [Chore] Cleanup repeated CIs (#399) --- .github/workflows/anchor_platform_integration_check.yml | 4 ---- .github/workflows/ci.yml | 4 ---- .github/workflows/e2e_integration_test.yml | 4 ---- 3 files changed, 12 deletions(-) diff --git a/.github/workflows/anchor_platform_integration_check.yml b/.github/workflows/anchor_platform_integration_check.yml index 8ccd9f7c7..765e94651 100644 --- a/.github/workflows/anchor_platform_integration_check.yml +++ b/.github/workflows/anchor_platform_integration_check.yml @@ -4,10 +4,6 @@ on: push: branches: - main - - develop - - "release/**" - - "releases/**" - - "hotfix/**" pull_request: workflow_call: # allows this workflow to be called from another workflow diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f793535b6..666456ab5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,6 @@ on: push: branches: - main - - develop - - "release/**" - - "releases/**" - - "hotfix/**" pull_request: workflow_call: # allows this workflow to be called from another workflow diff --git a/.github/workflows/e2e_integration_test.yml b/.github/workflows/e2e_integration_test.yml index 4a5ad80e9..4799061b1 100644 --- a/.github/workflows/e2e_integration_test.yml +++ b/.github/workflows/e2e_integration_test.yml @@ -4,10 +4,6 @@ on: push: branches: - main - - develop - - "release/**" - - "releases/**" - - "hotfix/**" pull_request: workflow_call: # allows this workflow to be called from another workflow From dd06d2ddb7ea721b836b02b405b8dcdebb3f7c2b Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Fri, 23 Aug 2024 10:42:25 -0700 Subject: [PATCH 16/75] [SDP-1307] track verification channel (#402) * SDP-1307 track verification channel * SDP-1307 address PR reviews --- ...cations-table-add-verification-channel.sql | 12 +++ internal/data/receiver_verification.go | 87 ++++++++++++++----- internal/data/receiver_verification_test.go | 79 ++++++++++++++--- internal/data/receivers.go | 2 + .../verifiy_receiver_registration_handler.go | 30 +++++-- ...ifiy_receiver_registration_handler_test.go | 16 +++- internal/utils/utils.go | 5 ++ 7 files changed, 188 insertions(+), 43 deletions(-) create mode 100644 db/migrations/sdp-migrations/2024-08-20.0-alter-receiver-verifications-table-add-verification-channel.sql diff --git a/db/migrations/sdp-migrations/2024-08-20.0-alter-receiver-verifications-table-add-verification-channel.sql b/db/migrations/sdp-migrations/2024-08-20.0-alter-receiver-verifications-table-add-verification-channel.sql new file mode 100644 index 000000000..7bb48d38d --- /dev/null +++ b/db/migrations/sdp-migrations/2024-08-20.0-alter-receiver-verifications-table-add-verification-channel.sql @@ -0,0 +1,12 @@ +-- +migrate Up +ALTER TABLE receiver_verifications + ADD COLUMN verification_channel message_channel; + +UPDATE receiver_verifications rv + SET verification_channel = 'SMS'::message_channel + WHERE rv.verification_channel IS NULL AND confirmed_at IS NOT NULL; + +-- +migrate Down + +ALTER TABLE receiver_verifications + DROP COLUMN verification_channel; \ No newline at end of file diff --git a/internal/data/receiver_verification.go b/internal/data/receiver_verification.go index b93b33866..4a2b469e8 100644 --- a/internal/data/receiver_verification.go +++ b/internal/data/receiver_verification.go @@ -13,17 +13,19 @@ import ( "golang.org/x/crypto/bcrypt" "github.com/stellar/stellar-disbursement-platform-backend/db" + "github.com/stellar/stellar-disbursement-platform-backend/internal/message" ) type ReceiverVerification struct { - ReceiverID string `json:"receiver_id" db:"receiver_id"` - VerificationField VerificationField `json:"verification_field" db:"verification_field"` - HashedValue string `json:"hashed_value" db:"hashed_value"` - Attempts int `json:"attempts" db:"attempts"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - ConfirmedAt *time.Time `json:"confirmed_at" db:"confirmed_at"` - FailedAt *time.Time `json:"failed_at" db:"failed_at"` + ReceiverID string `json:"receiver_id" db:"receiver_id"` + VerificationField VerificationField `json:"verification_field" db:"verification_field"` + HashedValue string `json:"hashed_value" db:"hashed_value"` + Attempts int `json:"attempts" db:"attempts"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + ConfirmedAt *time.Time `json:"confirmed_at" db:"confirmed_at"` + FailedAt *time.Time `json:"failed_at" db:"failed_at"` + VerificationChannel *message.MessageChannel `json:"verification_channel" db:"verification_channel"` } type ReceiverVerificationModel struct { @@ -207,29 +209,68 @@ func (m *ReceiverVerificationModel) UpsertVerificationValue(ctx context.Context, return nil } +type ReceiverVerificationUpdate struct { + ReceiverID string `db:"receiver_id"` + VerificationField VerificationField `db:"verification_field"` + VerificationChannel message.MessageChannel `db:"verification_channel"` + Attempts *int `db:"attempts"` + ConfirmedAt *time.Time `db:"confirmed_at"` + FailedAt *time.Time `db:"failed_at"` +} + +func (rvu ReceiverVerificationUpdate) Validate() error { + if strings.TrimSpace(rvu.ReceiverID) == "" { + return fmt.Errorf("receiver id is required") + } + if rvu.VerificationField == "" { + return fmt.Errorf("verification field is required") + } + if rvu.VerificationChannel == "" { + return fmt.Errorf("verification channel is required") + } + return nil +} + // UpdateReceiverVerification updates the attempts, confirmed_at, and failed_at values of a receiver verification. -func (m *ReceiverVerificationModel) UpdateReceiverVerification(ctx context.Context, receiverVerification ReceiverVerification, sqlExec db.SQLExecuter) error { +func (m *ReceiverVerificationModel) UpdateReceiverVerification(ctx context.Context, update ReceiverVerificationUpdate, sqlExec db.SQLExecuter) error { + if err := update.Validate(); err != nil { + return fmt.Errorf("validating receiver verification update: %w", err) + } + + fields := []string{} + args := []interface{}{} + + if update.Attempts != nil { + fields = append(fields, "attempts = ?") + args = append(args, update.Attempts) + } + + if update.ConfirmedAt != nil { + fields = append(fields, "confirmed_at = ?") + args = append(args, update.ConfirmedAt) + } + + if update.FailedAt != nil { + fields = append(fields, "failed_at = ?") + args = append(args, update.FailedAt) + } + query := ` UPDATE receiver_verifications SET - attempts = $1, - confirmed_at = $2, - failed_at = $3 - WHERE - receiver_id = $4 AND verification_field = $5 + %s, + verification_channel = ? + WHERE + receiver_id = ? AND + verification_field = ? ` - _, err := sqlExec.ExecContext(ctx, - query, - receiverVerification.Attempts, - receiverVerification.ConfirmedAt, - receiverVerification.FailedAt, - receiverVerification.ReceiverID, - receiverVerification.VerificationField, - ) + args = append(args, update.VerificationChannel, update.ReceiverID, update.VerificationField) + query = sqlExec.Rebind(fmt.Sprintf(query, strings.Join(fields, ", "))) + _, err := sqlExec.ExecContext(ctx, query, args...) if err != nil { - return fmt.Errorf("error updating receiver verification: %w", err) + return fmt.Errorf("updating receiver verification: %w", err) } return nil diff --git a/internal/data/receiver_verification_test.go b/internal/data/receiver_verification_test.go index 86b98648c..8adf97d27 100644 --- a/internal/data/receiver_verification_test.go +++ b/internal/data/receiver_verification_test.go @@ -11,6 +11,8 @@ import ( "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/message" + "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" ) func Test_ReceiverVerificationModel_GetByReceiverIDsAndVerificationField(t *testing.T) { @@ -328,12 +330,11 @@ func Test_ReceiverVerificationModel_UpsertVerificationValue(t *testing.T) { // Receiver confirmed the verification value now := time.Now() - err = receiverVerificationModel.UpdateReceiverVerification(ctx, ReceiverVerification{ - ReceiverID: receiver.ID, - VerificationField: VerificationFieldNationalID, - Attempts: 0, - ConfirmedAt: &now, - FailedAt: nil, + err = receiverVerificationModel.UpdateReceiverVerification(ctx, ReceiverVerificationUpdate{ + ReceiverID: receiver.ID, + VerificationField: VerificationFieldNationalID, + ConfirmedAt: &now, + VerificationChannel: message.MessageChannelSMS, }, dbConnectionPool) require.NoError(t, err) @@ -379,11 +380,16 @@ func Test_ReceiverVerificationModel_UpdateReceiverVerification(t *testing.T) { assert.Equal(t, 0, verification.Attempts) date := time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC) - verification.Attempts = 5 - verification.ConfirmedAt = &date - verification.FailedAt = &date + verificationUpdate := ReceiverVerificationUpdate{ + ReceiverID: receiver.ID, + VerificationField: VerificationFieldDateOfBirth, + Attempts: utils.IntPtr(5), + ConfirmedAt: &date, + FailedAt: &date, + VerificationChannel: message.MessageChannelSMS, + } - err = receiverVerificationModel.UpdateReceiverVerification(ctx, *verification, dbConnectionPool) + err = receiverVerificationModel.UpdateReceiverVerification(ctx, verificationUpdate, dbConnectionPool) require.NoError(t, err) // validate if the receiver verification has been updated @@ -391,7 +397,8 @@ func Test_ReceiverVerificationModel_UpdateReceiverVerification(t *testing.T) { SELECT rv.attempts, rv.confirmed_at, - rv.failed_at + rv.failed_at, + rv.verification_channel FROM receiver_verifications rv WHERE @@ -404,6 +411,56 @@ func Test_ReceiverVerificationModel_UpdateReceiverVerification(t *testing.T) { assert.Equal(t, &date, receiverVerificationUpdated.ConfirmedAt) assert.Equal(t, &date, receiverVerificationUpdated.FailedAt) assert.Equal(t, 5, receiverVerificationUpdated.Attempts) + assert.Equal(t, message.MessageChannelSMS, *receiverVerificationUpdated.VerificationChannel) +} + +func Test_ReceiverVerificationUpdate_Validate(t *testing.T) { + tests := []struct { + name string + update ReceiverVerificationUpdate + wantErr error + }{ + { + name: "valid update", + update: ReceiverVerificationUpdate{ + ReceiverID: "receiver-id", + VerificationField: VerificationFieldDateOfBirth, + VerificationChannel: message.MessageChannelSMS, + }, + wantErr: nil, + }, + { + name: "invalid update with empty receiver id", + update: ReceiverVerificationUpdate{}, + wantErr: fmt.Errorf("receiver id is required"), + }, + { + name: "invalid update with empty verification field", + update: ReceiverVerificationUpdate{ + ReceiverID: "receiver-id", + }, + wantErr: fmt.Errorf("verification field is required"), + }, + { + name: "invalid update with empty verification channel", + update: ReceiverVerificationUpdate{ + ReceiverID: "receiver-id", + VerificationField: VerificationFieldDateOfBirth, + }, + wantErr: fmt.Errorf("verification channel is required"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.update.Validate() + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } } func Test_ReceiverVerificationModel_CheckTotalAttempts(t *testing.T) { diff --git a/internal/data/receivers.go b/internal/data/receivers.go index 9beb4a3ac..c21be43fd 100644 --- a/internal/data/receivers.go +++ b/internal/data/receivers.go @@ -26,6 +26,8 @@ type Receiver struct { } type ReceiverRegistrationRequest struct { + // TODO: SDP-1296 - Update `/wallet-registration/otp` to support multiple contact information types and send OTPs accordingly + Email string `json:"email"` PhoneNumber string `json:"phone_number"` OTP string `json:"otp"` VerificationValue string `json:"verification"` diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler.go b/internal/serve/httphandler/verifiy_receiver_registration_handler.go index 3f47552c3..9b16214cd 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/stellar/go/support/log" @@ -17,6 +18,7 @@ import ( "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/events/schemas" + "github.com/stellar/stellar-disbursement-platform-backend/internal/message" "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/transactionsubmission/engine/signing" @@ -138,15 +140,28 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( } // STEP 3: check if the payload verification value matches the one saved in the database + rvu := data.ReceiverVerificationUpdate{ + ReceiverID: receiverVerification.ReceiverID, + VerificationField: receiverVerification.VerificationField, + } + + if strings.TrimSpace(receiverRegistrationRequest.PhoneNumber) != "" { + rvu.VerificationChannel = message.MessageChannelSMS + } else if strings.TrimSpace(receiverRegistrationRequest.Email) != "" { + rvu.VerificationChannel = message.MessageChannelEmail + } else { + err = fmt.Errorf("no valid verification channel found resolved for receiver") + return &ErrorInformationNotFound{cause: err} + } + if !data.CompareVerificationValue(receiverVerification.HashedValue, receiverRegistrationRequest.VerificationValue) { baseErrMsg := fmt.Sprintf("%s value does not match for user with phone number %s", receiverRegistrationRequest.VerificationType, truncatedPhoneNumber) // update the receiver verification with the confirmation that the value was checked - receiverVerification.Attempts = receiverVerification.Attempts + 1 - receiverVerification.FailedAt = &now - receiverVerification.ConfirmedAt = nil + rvu.Attempts = utils.IntPtr(receiverVerification.Attempts + 1) + rvu.FailedAt = &now - // this update is done using the DBConnectionPool and not dbTx because we don't want to roolback these changes after returning the error - updateErr := v.Models.ReceiverVerification.UpdateReceiverVerification(ctx, *receiverVerification, v.Models.DBConnectionPool) + // this update is done using the DBConnectionPool and not dbTx because we don't want to rollback these changes after returning the error + updateErr := v.Models.ReceiverVerification.UpdateReceiverVerification(ctx, rvu, v.Models.DBConnectionPool) if updateErr != nil { err = fmt.Errorf("%s: %w", baseErrMsg, updateErr) } else { @@ -158,8 +173,9 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( // STEP 4: update the receiver verification row with the confirmation that the value was successfully validated if receiverVerification.ConfirmedAt == nil { - receiverVerification.ConfirmedAt = &now - err = v.Models.ReceiverVerification.UpdateReceiverVerification(ctx, *receiverVerification, dbTx) + rvu.ConfirmedAt = &now + + err = v.Models.ReceiverVerification.UpdateReceiverVerification(ctx, rvu, dbTx) if err != nil { return fmt.Errorf("updating successfully verified user: %w", err) } diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go index 8b2600a18..a842a0d2c 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go @@ -27,9 +27,11 @@ import ( "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/events/schemas" + "github.com/stellar/stellar-disbursement-platform-backend/internal/message" "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror" "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/validators" 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-multitenant/pkg/tenant" ) @@ -207,7 +209,12 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te VerificationValue: "1990-01-01", }) receiverVerificationExceededAttempts.Attempts = data.MaxAttemptsAllowed - err = models.ReceiverVerification.UpdateReceiverVerification(ctx, *receiverVerificationExceededAttempts, dbConnectionPool) + err = models.ReceiverVerification.UpdateReceiverVerification(ctx, data.ReceiverVerificationUpdate{ + ReceiverID: receiverWithExceededAttempts.ID, + VerificationField: data.VerificationFieldDateOfBirth, + Attempts: utils.IntPtr(data.MaxAttemptsAllowed), + VerificationChannel: message.MessageChannelSMS, + }, dbConnectionPool) require.NoError(t, err) // receiver with receiver_verification row: @@ -902,7 +909,12 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin VerificationValue: "1990-01-01", }) receiverVerificationExceededAttempts.Attempts = data.MaxAttemptsAllowed - err = models.ReceiverVerification.UpdateReceiverVerification(ctx, *receiverVerificationExceededAttempts, dbConnectionPool) + err = models.ReceiverVerification.UpdateReceiverVerification(ctx, data.ReceiverVerificationUpdate{ + ReceiverID: receiverWithExceededAttempts.ID, + VerificationField: data.VerificationFieldDateOfBirth, + Attempts: utils.IntPtr(data.MaxAttemptsAllowed), + VerificationChannel: message.MessageChannelSMS, + }, dbConnectionPool) require.NoError(t, err) // set the logger to a buffer so we can check the error message diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 3bcca2950..35211733d 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -95,6 +95,11 @@ func StringPtr(s string) *string { return &s } +// IntPtr returns a pointer to an int +func IntPtr(i int) *int { + return &i +} + func TimePtr(t time.Time) *time.Time { return &t } From 54af94505189f6a5509d8b982c34e821eba72dba Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Fri, 23 Aug 2024 13:24:34 -0700 Subject: [PATCH 17/75] [SDP-1203] Add Circle Metrics (#385) * SDP-1203 Add Circle Metrics * SDP-1203 address PR reviews --- cmd/serve.go | 1 + dev/README.md | 27 + dev/docker-compose-monitoring.yml | 23 + dev/docker-compose-sdp-anchor.yml | 10 +- dev/grafana/datasource.yaml | 9 + dev/prometheus/prometheus.yml | 8 + internal/circle/client.go | 74 ++- internal/circle/client_test.go | 218 +++++-- internal/circle/service.go | 15 +- internal/circle/service_test.go | 41 +- .../circle_service_test.go | 2 + internal/monitor/main.go | 2 +- internal/monitor/metric_tags.go | 5 + internal/monitor/monitor_labels.go | 20 + internal/monitor/prometheus_client.go | 23 +- internal/monitor/prometheus_client_test.go | 2 +- internal/monitor/prometheus_metrics.go | 43 +- internal/monitor/utils.go | 19 + .../httphandler/circle_config_handler.go | 9 +- .../httphandler/circle_config_handler_test.go | 4 +- internal/serve/serve.go | 1 + resources/grafana/dashboard.json | 551 +++++++++++++----- 22 files changed, 848 insertions(+), 259 deletions(-) create mode 100644 dev/docker-compose-monitoring.yml create mode 100644 dev/grafana/datasource.yaml create mode 100644 dev/prometheus/prometheus.yml create mode 100644 internal/monitor/utils.go diff --git a/cmd/serve.go b/cmd/serve.go index 48db2925a..87ccb2a1b 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -631,6 +631,7 @@ func (c *ServeCommand) Command(serverService ServerServiceInterface, monitorServ NetworkType: serveOpts.NetworkType, EncryptionPassphrase: serveOpts.DistAccEncryptionPassphrase, TenantManager: tenant.NewManager(tenant.WithDatabase(serveOpts.AdminDBConnectionPool)), + MonitorService: serveOpts.MonitorService, }) if err != nil { log.Ctx(ctx).Fatalf("error creating Circle service: %v", err) diff --git a/dev/README.md b/dev/README.md index eca0359b4..5a15af089 100644 --- a/dev/README.md +++ b/dev/README.md @@ -20,6 +20,7 @@ - [Ensure Docker Containers are Running:](#ensure-docker-containers-are-running) - [Using VS Code:](#using-vs-code) - [Using IntelliJ GoLang:](#using-intellij-golang) + - [Monitoring the SDP](#monitoring-the-sdp) - [Troubleshooting](#troubleshooting) - [Sample Tenant Management Postman collection](#sample-tenant-management-postman-collection) - [Distribution account out of funds](#distribution-account-out-of-funds) @@ -286,6 +287,32 @@ Make sure the Docker containers are up and running by executing the `main.sh` sc The debugger should now attach to the running Docker container, and you should be able to hit breakpoints and debug your code. +### Monitoring the SDP + +The SDP supports monitoring via Prometheus and Grafana. + +#### Start Prometheus and Grafana containers + +The containers can be started by running the following command from the `dev` directory: + +```sh +docker compose -p sdp-multi-tenant -f docker-compose-monitoring.yml up -d +``` + +This will start the following services: +* `prometheus`: Prometheus service running on port `9090`. +* `grafana`: Grafana service running on port `3002`. + +#### Load the SDP Grafana Dashboard + +1. Access the Grafana dashboard by opening a browser and going to [http://localhost:3002](http://localhost:3002). +2. Log in with the default credentials: + - Username: `admin` + - Password: `admin` +3. Click on the `+` icon on the left sidebar and select `Import Dashboard`. +4. Copy the contents of the [dashboard.json](../resources/grafana/dashboard.json) file and paste it into the `Import via dashboard JSON model` text box. + + ## Troubleshooting #### Sample Tenant Management Postman collection diff --git a/dev/docker-compose-monitoring.yml b/dev/docker-compose-monitoring.yml new file mode 100644 index 000000000..a7cd22cd6 --- /dev/null +++ b/dev/docker-compose-monitoring.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + prometheus: + image: prom/prometheus:latest + volumes: + - ./prometheus:/etc/prometheus/ + ports: + - "9090:9090" + + grafana: + image: grafana/grafana:latest + ports: + - "3002:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana-storage:/var/lib/grafana + - ./grafana/datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yaml + +volumes: + grafana-storage: \ No newline at end of file diff --git a/dev/docker-compose-sdp-anchor.yml b/dev/docker-compose-sdp-anchor.yml index dc5ac650a..27ae51894 100644 --- a/dev/docker-compose-sdp-anchor.yml +++ b/dev/docker-compose-sdp-anchor.yml @@ -51,12 +51,12 @@ services: SINGLE_TENANT_MODE: "false" # scheduler options - #ENABLE_SCHEDULER: "false" # set to disabled to use kafka for this. - #SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS: "10" - #SCHEDULER_PAYMENT_JOB_SECONDS: "10" + ENABLE_SCHEDULER: "true" # set to disabled to use kafka for this. + SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS: "10" + SCHEDULER_PAYMENT_JOB_SECONDS: "10" # Kafka Configuration - only used if ENABLE_SCHEDULER is set to "false" - EVENT_BROKER_TYPE: "KAFKA" + EVENT_BROKER_TYPE: "NONE" BROKER_URLS: "kafka:9092" CONSUMER_GROUP_ID: "group-id" KAFKA_SECURITY_PROTOCOL: "PLAINTEXT" @@ -118,7 +118,7 @@ services: DATA_DATABASE: postgres DATA_FLYWAY_ENABLED: "true" DATA_DDL_AUTO: update - METRICS_ENABLED: "false" # Metrics would be available at port 8082 + METRICS_ENABLED: "true" # Metrics would be available at port 8082 METRICS_EXTRAS_ENABLED: "false" SEP10_ENABLED: "true" SEP10_HOME_DOMAINS: "localhost:8000, *.stellar.local:8000" # Comma separated list of home domains diff --git a/dev/grafana/datasource.yaml b/dev/grafana/datasource.yaml new file mode 100644 index 000000000..9c1d44c4e --- /dev/null +++ b/dev/grafana/datasource.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: prometheus + type: prometheus + access: proxy + url: http://host.docker.internal:9090 + isDefault: true + editable: true \ No newline at end of file diff --git a/dev/prometheus/prometheus.yml b/dev/prometheus/prometheus.yml new file mode 100644 index 000000000..66c78f914 --- /dev/null +++ b/dev/prometheus/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'application' + metrics_path: /metrics + static_configs: + - targets: ['host.docker.internal:8002'] \ No newline at end of file diff --git a/internal/circle/client.go b/internal/circle/client.go index 0eace1e58..21121140e 100644 --- a/internal/circle/client.go +++ b/internal/circle/client.go @@ -15,6 +15,7 @@ import ( "github.com/avast/retry-go" "github.com/stellar/go/support/log" + "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor" "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httpclient" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" "github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant" @@ -40,29 +41,38 @@ type ClientInterface interface { // Client provides methods to interact with the Circle API. type Client struct { - BasePath string - APIKey string - httpClient httpclient.HttpClientInterface - tenantManager tenant.ManagerInterface + BasePath string + APIKey string + httpClient httpclient.HttpClientInterface + tenantManager tenant.ManagerInterface + monitorService monitor.MonitorServiceInterface } // ClientFactory is a function that creates a ClientInterface. -type ClientFactory func(networkType utils.NetworkType, apiKey string, tntManager tenant.ManagerInterface) ClientInterface +type ClientFactory func(opts ClientOptions) ClientInterface var _ ClientFactory = NewClient +type ClientOptions struct { + NetworkType utils.NetworkType + APIKey string + TenantManager tenant.ManagerInterface + MonitorService monitor.MonitorServiceInterface +} + // NewClient creates a new instance of Circle Client. -func NewClient(networkType utils.NetworkType, apiKey string, tntManager tenant.ManagerInterface) ClientInterface { +func NewClient(opts ClientOptions) ClientInterface { circleEnv := Sandbox - if networkType == utils.PubnetNetworkType { + if opts.NetworkType == utils.PubnetNetworkType { circleEnv = Production } return &Client{ - BasePath: string(circleEnv), - APIKey: apiKey, - httpClient: httpclient.DefaultClient(), - tenantManager: tntManager, + BasePath: string(circleEnv), + APIKey: opts.APIKey, + httpClient: httpclient.DefaultClient(), + tenantManager: opts.TenantManager, + monitorService: opts.MonitorService, } } @@ -75,7 +85,7 @@ func (client *Client) Ping(ctx context.Context) (bool, error) { return false, fmt.Errorf("building path: %w", err) } - resp, err := client.request(ctx, u, http.MethodGet, false, nil) + resp, err := client.request(ctx, pingPath, u, http.MethodGet, false, nil) if err != nil { return false, fmt.Errorf("making request: %w", err) } @@ -118,7 +128,7 @@ func (client *Client) PostTransfer(ctx context.Context, transferReq TransferRequ return nil, err } - resp, err := client.request(ctx, u, http.MethodPost, true, transferData) + resp, err := client.request(ctx, transferPath, u, http.MethodPost, true, transferData) if err != nil { return nil, fmt.Errorf("making request: %w", err) } @@ -142,7 +152,7 @@ func (client *Client) GetTransferByID(ctx context.Context, id string) (*Transfer return nil, fmt.Errorf("building path: %w", err) } - resp, err := client.request(ctx, u, http.MethodGet, true, nil) + resp, err := client.request(ctx, transferPath, u, http.MethodGet, true, nil) if err != nil { return nil, fmt.Errorf("making request: %w", err) } @@ -166,7 +176,7 @@ func (client *Client) GetWalletByID(ctx context.Context, id string) (*Wallet, er return nil, fmt.Errorf("building path: %w", err) } - resp, err := client.request(ctx, url, http.MethodGet, true, nil) + resp, err := client.request(ctx, walletPath, url, http.MethodGet, true, nil) if err != nil { return nil, fmt.Errorf("making request: %w", err) } @@ -193,10 +203,11 @@ func (re RetryableError) Error() string { } // request makes an HTTP request to the Circle API. -func (client *Client) request(ctx context.Context, u string, method string, isAuthed bool, bodyBytes []byte) (*http.Response, error) { +func (client *Client) request(ctx context.Context, path, u, method string, isAuthed bool, bodyBytes []byte) (*http.Response, error) { var resp *http.Response err := retry.Do( func() error { + startTime := time.Now() bodyReader := bytes.NewReader(bodyBytes) req, err := http.NewRequestWithContext(ctx, method, u, bodyReader) if err != nil { @@ -212,6 +223,8 @@ func (client *Client) request(ctx context.Context, u string, method string, isAu } resp, err = client.httpClient.Do(req) + client.recordCircleAPIMetrics(ctx, method, path, startTime, resp, err) + if err != nil { return fmt.Errorf("submitting request to %s: %w", u, err) } @@ -264,6 +277,33 @@ func parseRetryAfter(retryAfter string) time.Duration { return time.Duration(seconds) * time.Second } +func (client *Client) recordCircleAPIMetrics(ctx context.Context, method, endpoint string, startTime time.Time, resp *http.Response, reqErr error) { + t, err := tenant.GetTenantFromContext(ctx) + if err != nil { + log.Ctx(ctx).Errorf("getting tenant from context: %v", err) + return + } + + duration := time.Since(startTime) + status, statusCode := monitor.ParseHTTPResponseStatus(resp, reqErr) + + labels := monitor.CircleLabels{ + Method: method, + Endpoint: endpoint, + Status: status, + StatusCode: statusCode, + TenantName: t.Name, + }.ToMap() + + if monitorErr := client.monitorService.MonitorHistogram(duration.Seconds(), monitor.CircleAPIRequestDurationTag, labels); monitorErr != nil { + log.Ctx(ctx).Errorf("monitoring histogram: %v", err) + } + + if monitorErr := client.monitorService.MonitorCounters(monitor.CircleAPIRequestsTotalTag, labels); monitorErr != nil { + log.Ctx(ctx).Errorf("monitoring counter: %v", err) + } +} + func (client *Client) handleError(ctx context.Context, resp *http.Response) error { if slices.Contains(authErrorStatusCodes, resp.StatusCode) { tnt, getCtxTntErr := tenant.GetTenantFromContext(ctx) @@ -282,7 +322,7 @@ func (client *Client) handleError(ctx context.Context, resp *http.Response) erro return fmt.Errorf("parsing API error: %w", err) } - return fmt.Errorf("Circle API error: %w", apiError) //nolint:golint,unused + return fmt.Errorf("circle API error: %w", apiError) } var _ ClientInterface = (*Client)(nil) diff --git a/internal/circle/client_test.go b/internal/circle/client_test.go index f6959fa34..bbddc803c 100644 --- a/internal/circle/client_test.go +++ b/internal/circle/client_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "strconv" "testing" "github.com/google/uuid" @@ -14,6 +15,8 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor" + monitorMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor/mocks" httpclientMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httpclient/mocks" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" "github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant" @@ -21,8 +24,14 @@ import ( func Test_NewClient(t *testing.T) { mockTntManager := &tenant.TenantManagerMock{} + mMonitorService := monitorMocks.NewMockMonitorService(t) t.Run("production environment", func(t *testing.T) { - clientInterface := NewClient(utils.PubnetNetworkType, "test-key", mockTntManager) + clientInterface := NewClient(ClientOptions{ + NetworkType: utils.PubnetNetworkType, + APIKey: "test-key", + TenantManager: mockTntManager, + MonitorService: mMonitorService, + }) cc, ok := clientInterface.(*Client) assert.True(t, ok) assert.Equal(t, string(Production), cc.BasePath) @@ -30,7 +39,12 @@ func Test_NewClient(t *testing.T) { }) t.Run("sandbox environment", func(t *testing.T) { - clientInterface := NewClient(utils.TestnetNetworkType, "test-key", mockTntManager) + clientInterface := NewClient(ClientOptions{ + NetworkType: utils.TestnetNetworkType, + APIKey: "test-key", + TenantManager: mockTntManager, + MonitorService: mMonitorService, + }) cc, ok := clientInterface.(*Client) assert.True(t, ok) assert.Equal(t, string(Sandbox), cc.BasePath) @@ -42,9 +56,9 @@ func Test_Client_Ping(t *testing.T) { ctx := context.Background() t.Run("ping error", func(t *testing.T) { - cc, httpClientMock, _ := newClientWithMocks(t) + cc, cMocks := newClientWithMocks(t) testError := errors.New("test error") - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Return(nil, testError). Once() @@ -55,8 +69,8 @@ func Test_Client_Ping(t *testing.T) { }) t.Run("ping successful", func(t *testing.T) { - cc, httpClientMock, _ := newClientWithMocks(t) - httpClientMock. + cc, cMocks := newClientWithMocks(t) + cMocks.httpClientMock. On("Do", mock.Anything). Return(&http.Response{ StatusCode: http.StatusOK, @@ -88,9 +102,9 @@ func Test_Client_PostTransfer(t *testing.T) { } t.Run("post transfer error", func(t *testing.T) { - cc, httpClientMock, _ := newClientWithMocks(t) + cc, cMocks := newClientWithMocks(t) testError := errors.New("test error") - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Return(nil, testError). Once() @@ -101,7 +115,7 @@ func Test_Client_PostTransfer(t *testing.T) { }) t.Run("post transfer fails to validate request", func(t *testing.T) { - cc, _, _ := newClientWithMocks(t) + cc, _ := newClientWithMocks(t) transfer, err := cc.PostTransfer(ctx, TransferRequest{}) assert.EqualError(t, err, fmt.Errorf("validating transfer request: %w", errors.New("source type must be provided")).Error()) assert.Nil(t, transfer) @@ -109,29 +123,45 @@ func Test_Client_PostTransfer(t *testing.T) { t.Run("post transfer fails auth", func(t *testing.T) { unauthorizedResponse := `{"code": 401, "message": "Malformed key. Does it contain three parts?"}` - cc, httpClientMock, tntManagerMock := newClientWithMocks(t) - tnt := &tenant.Tenant{ID: "test-id"} + cc, cMocks := newClientWithMocks(t) + tnt := &tenant.Tenant{ID: "test-id", Name: "test-tenant"} ctx = tenant.SaveTenantInContext(ctx, tnt) - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Return(&http.Response{ StatusCode: http.StatusUnauthorized, Body: io.NopCloser(bytes.NewBufferString(unauthorizedResponse)), }, nil). Once() - tntManagerMock. + cMocks.tenantManagerMock. On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). Return(nil).Once() + expectedLabels := map[string]string{ + "endpoint": transferPath, + "method": http.MethodPost, + "status": "success", + "status_code": strconv.Itoa(http.StatusUnauthorized), + "tenant_name": tnt.Name, + } + cMocks.monitorServiceMock. + On("MonitorHistogram", mock.Anything, monitor.CircleAPIRequestDurationTag, expectedLabels). + Return(nil).Once() + cMocks.monitorServiceMock. + On("MonitorCounters", monitor.CircleAPIRequestsTotalTag, expectedLabels). + Return(nil).Once() transfer, err := cc.PostTransfer(ctx, validTransferReq) - assert.EqualError(t, err, "handling API response error: Circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") + assert.EqualError(t, err, "handling API response error: circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") assert.Nil(t, transfer) }) t.Run("post transfer successful", func(t *testing.T) { - cc, httpClientMock, _ := newClientWithMocks(t) - httpClientMock. + cc, cMocks := newClientWithMocks(t) + tnt := &tenant.Tenant{ID: "test-id", Name: "test-tenant"} + ctx = tenant.SaveTenantInContext(ctx, tnt) + + cMocks.httpClientMock. On("Do", mock.Anything). Return(&http.Response{ StatusCode: http.StatusCreated, @@ -148,6 +178,20 @@ func Test_Client_PostTransfer(t *testing.T) { }). Once() + expectedLabels := map[string]string{ + "endpoint": transferPath, + "method": http.MethodPost, + "status": "success", + "status_code": strconv.Itoa(http.StatusCreated), + "tenant_name": "test-tenant", + } + cMocks.monitorServiceMock. + On("MonitorHistogram", mock.Anything, monitor.CircleAPIRequestDurationTag, expectedLabels). + Return(nil).Once() + cMocks.monitorServiceMock. + On("MonitorCounters", monitor.CircleAPIRequestsTotalTag, expectedLabels). + Return(nil).Once() + transfer, err := cc.PostTransfer(ctx, validTransferReq) assert.NoError(t, err) assert.Equal(t, "test-id", transfer.ID) @@ -157,9 +201,9 @@ func Test_Client_PostTransfer(t *testing.T) { func Test_Client_GetTransferByID(t *testing.T) { ctx := context.Background() t.Run("get transfer by id error", func(t *testing.T) { - cc, httpClientMock, _ := newClientWithMocks(t) + cc, cMocks := newClientWithMocks(t) testError := errors.New("test error") - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Return(nil, testError). Once() @@ -171,29 +215,45 @@ func Test_Client_GetTransferByID(t *testing.T) { t.Run("get transfer by id fails auth", func(t *testing.T) { unauthorizedResponse := `{"code": 401, "message": "Malformed key. Does it contain three parts?"}` - cc, httpClientMock, tntManagerMock := newClientWithMocks(t) - tnt := &tenant.Tenant{ID: "test-id"} + cc, cMocks := newClientWithMocks(t) + tnt := &tenant.Tenant{ID: "test-id", Name: "test-tenant"} ctx = tenant.SaveTenantInContext(ctx, tnt) - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Return(&http.Response{ StatusCode: http.StatusUnauthorized, Body: io.NopCloser(bytes.NewBufferString(unauthorizedResponse)), }, nil). Once() - tntManagerMock. + cMocks.tenantManagerMock. On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). Return(nil).Once() + expectedLabels := map[string]string{ + "endpoint": transferPath, + "method": http.MethodGet, + "status": "success", + "status_code": strconv.Itoa(http.StatusUnauthorized), + "tenant_name": tnt.Name, + } + cMocks.monitorServiceMock. + On("MonitorHistogram", mock.Anything, monitor.CircleAPIRequestDurationTag, expectedLabels). + Return(nil).Once() + cMocks.monitorServiceMock. + On("MonitorCounters", monitor.CircleAPIRequestsTotalTag, expectedLabels). + Return(nil).Once() transfer, err := cc.GetTransferByID(ctx, "test-id") - assert.EqualError(t, err, "handling API response error: Circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") + assert.EqualError(t, err, "handling API response error: circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") assert.Nil(t, transfer) }) t.Run("get transfer by id successful", func(t *testing.T) { - cc, httpClientMock, _ := newClientWithMocks(t) - httpClientMock. + cc, cMocks := newClientWithMocks(t) + tnt := &tenant.Tenant{ID: "test-id", Name: "test-tenant"} + ctx = tenant.SaveTenantInContext(ctx, tnt) + + cMocks.httpClientMock. On("Do", mock.Anything). Return(&http.Response{ StatusCode: http.StatusOK, @@ -209,6 +269,20 @@ func Test_Client_GetTransferByID(t *testing.T) { }). Once() + expectedLabels := map[string]string{ + "endpoint": transferPath, + "method": http.MethodGet, + "status": "success", + "status_code": strconv.Itoa(http.StatusOK), + "tenant_name": tnt.Name, + } + cMocks.monitorServiceMock. + On("MonitorHistogram", mock.Anything, monitor.CircleAPIRequestDurationTag, expectedLabels). + Return(nil).Once() + cMocks.monitorServiceMock. + On("MonitorCounters", monitor.CircleAPIRequestsTotalTag, expectedLabels). + Return(nil).Once() + transfer, err := cc.GetTransferByID(ctx, "test-id") assert.NoError(t, err) assert.Equal(t, "test-id", transfer.ID) @@ -218,9 +292,9 @@ func Test_Client_GetTransferByID(t *testing.T) { func Test_Client_GetWalletByID(t *testing.T) { ctx := context.Background() t.Run("get wallet by id error", func(t *testing.T) { - cc, httpClientMock, _ := newClientWithMocks(t) + cc, cMocks := newClientWithMocks(t) testError := errors.New("test error") - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Run(func(args mock.Arguments) { req, ok := args.Get(0).(*http.Request) @@ -243,23 +317,36 @@ func Test_Client_GetWalletByID(t *testing.T) { "code": 401, "message": "Malformed key. Does it contain three parts?" }` - cc, httpClientMock, tntManagerMock := newClientWithMocks(t) + cc, cMocks := newClientWithMocks(t) tnt := &tenant.Tenant{ID: "test-id"} ctx = tenant.SaveTenantInContext(ctx, tnt) - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Return(&http.Response{ StatusCode: http.StatusUnauthorized, Body: io.NopCloser(bytes.NewBufferString(unauthorizedResponse)), }, nil). Once() - tntManagerMock. + cMocks.tenantManagerMock. On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). Return(nil).Once() + expectedLabels := map[string]string{ + "endpoint": walletPath, + "method": http.MethodGet, + "status": "success", + "status_code": strconv.Itoa(http.StatusUnauthorized), + "tenant_name": tnt.Name, + } + cMocks.monitorServiceMock. + On("MonitorHistogram", mock.Anything, monitor.CircleAPIRequestDurationTag, expectedLabels). + Return(nil).Once() + cMocks.monitorServiceMock. + On("MonitorCounters", monitor.CircleAPIRequestsTotalTag, expectedLabels). + Return(nil).Once() transfer, err := cc.GetWalletByID(ctx, "test-id") - assert.EqualError(t, err, "handling API response error: Circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") + assert.EqualError(t, err, "handling API response error: circle API error: APIError: Code=401, Message=Malformed key. Does it contain three parts?, Errors=[], StatusCode=401") assert.Nil(t, transfer) }) @@ -278,8 +365,10 @@ func Test_Client_GetWalletByID(t *testing.T) { ] } }` - cc, httpClientMock, _ := newClientWithMocks(t) - httpClientMock. + cc, cMocks := newClientWithMocks(t) + tnt := &tenant.Tenant{ID: "test-id", Name: "test-tenant"} + ctx = tenant.SaveTenantInContext(ctx, tnt) + cMocks.httpClientMock. On("Do", mock.Anything). Return(&http.Response{ StatusCode: http.StatusOK, @@ -294,7 +383,19 @@ func Test_Client_GetWalletByID(t *testing.T) { assert.Equal(t, "Bearer test-key", req.Header.Get("Authorization")) }). Once() - + expectedLabels := map[string]string{ + "endpoint": walletPath, + "method": http.MethodGet, + "status": "success", + "status_code": strconv.Itoa(http.StatusOK), + "tenant_name": tnt.Name, + } + cMocks.monitorServiceMock. + On("MonitorHistogram", mock.Anything, monitor.CircleAPIRequestDurationTag, expectedLabels). + Return(nil).Once() + cMocks.monitorServiceMock. + On("MonitorCounters", monitor.CircleAPIRequestsTotalTag, expectedLabels). + Return(nil).Once() wallet, err := cc.GetWalletByID(ctx, "test-id") assert.NoError(t, err) wantWallet := &Wallet{ @@ -315,11 +416,11 @@ func Test_Client_handleError(t *testing.T) { tnt := &tenant.Tenant{ID: "test-id"} ctx = tenant.SaveTenantInContext(ctx, tnt) - cc, _, tntManagerMock := newClientWithMocks(t) + cc, cMocks := newClientWithMocks(t) t.Run("deactivate tenant distribution account error", func(t *testing.T) { testError := errors.New("foo") - tntManagerMock. + cMocks.tenantManagerMock. On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). Return(testError).Once() @@ -332,12 +433,12 @@ func Test_Client_handleError(t *testing.T) { StatusCode: http.StatusUnauthorized, Body: io.NopCloser(bytes.NewBufferString(`{"code": 401, "message": "Unauthorized"}`)), } - tntManagerMock. + cMocks.tenantManagerMock. On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). Return(nil).Once() err := cc.handleError(ctx, unauthorizedResponse) - assert.EqualError(t, fmt.Errorf("Circle API error: %w", errors.New("APIError: Code=401, Message=Unauthorized, Errors=[], StatusCode=401")), err.Error()) + assert.EqualError(t, fmt.Errorf("circle API error: %w", errors.New("APIError: Code=401, Message=Unauthorized, Errors=[], StatusCode=401")), err.Error()) }) t.Run("deactivates tenant distribution account if Circle error response is forbidden", func(t *testing.T) { @@ -345,12 +446,12 @@ func Test_Client_handleError(t *testing.T) { StatusCode: http.StatusForbidden, Body: io.NopCloser(bytes.NewBufferString(`{"code": 403, "message": "Forbidden"}`)), } - tntManagerMock. + cMocks.tenantManagerMock. On("DeactivateTenantDistributionAccount", mock.Anything, tnt.ID). Return(nil).Once() err := cc.handleError(ctx, unauthorizedResponse) - assert.EqualError(t, fmt.Errorf("Circle API error: %w", errors.New("APIError: Code=403, Message=Forbidden, Errors=[], StatusCode=403")), err.Error()) + assert.EqualError(t, fmt.Errorf("circle API error: %w", errors.New("APIError: Code=403, Message=Forbidden, Errors=[], StatusCode=403")), err.Error()) }) t.Run("does not deactivate tenant distribution account if Circle error response is not unauthorized or forbidden", func(t *testing.T) { @@ -360,7 +461,7 @@ func Test_Client_handleError(t *testing.T) { } err := cc.handleError(ctx, unauthorizedResponse) - assert.EqualError(t, fmt.Errorf("Circle API error: %w", errors.New("APIError: Code=400, Message=Bad Request, Errors=[], StatusCode=400")), err.Error()) + assert.EqualError(t, fmt.Errorf("circle API error: %w", errors.New("APIError: Code=400, Message=Bad Request, Errors=[], StatusCode=400")), err.Error()) }) t.Run("records error correctly when not proper json", func(t *testing.T) { @@ -370,10 +471,10 @@ func Test_Client_handleError(t *testing.T) { } err := cc.handleError(ctx, unauthorizedResponse) - assert.EqualError(t, fmt.Errorf("Circle API error: %w", errors.New("APIError: Code=0, Message=error code: 1015, Errors=[], StatusCode=429")), err.Error()) + assert.EqualError(t, fmt.Errorf("circle API error: %w", errors.New("APIError: Code=0, Message=error code: 1015, Errors=[], StatusCode=429")), err.Error()) }) - tntManagerMock.AssertExpectations(t) + cMocks.tenantManagerMock.AssertExpectations(t) } func Test_Client_request(t *testing.T) { @@ -433,7 +534,8 @@ func Test_Client_request(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - cc, httpClientMock, _ := newClientWithMocks(t) + cc, cMocks := newClientWithMocks(t) + httpClientMock := cMocks.httpClientMock ctx := context.Background() u := "https://api-sandbox.circle.com/test" @@ -442,12 +544,12 @@ func Test_Client_request(t *testing.T) { body := []byte("test-body") for _, resp := range tt.responses { - httpClientMock. + cMocks.httpClientMock. On("Do", mock.Anything). Return(&resp, nil).Once() } - resp, err := cc.request(ctx, u, method, isAuthed, body) + resp, err := cc.request(ctx, "/test", u, method, isAuthed, body) if tt.expectedError != "" { require.Error(t, err) @@ -470,14 +572,26 @@ func Test_Client_request(t *testing.T) { } } -func newClientWithMocks(t *testing.T) (Client, *httpclientMocks.HttpClientMock, *tenant.TenantManagerMock) { +func newClientWithMocks(t *testing.T) (Client, *clientMocks) { httpClientMock := httpclientMocks.NewHttpClientMock(t) tntManagerMock := tenant.NewTenantManagerMock(t) + monitorSvcMock := monitorMocks.NewMockMonitorService(t) return Client{ - BasePath: "http://localhost:8080", - APIKey: "test-key", - httpClient: httpClientMock, - tenantManager: tntManagerMock, - }, httpClientMock, tntManagerMock + BasePath: "http://localhost:8080", + APIKey: "test-key", + httpClient: httpClientMock, + tenantManager: tntManagerMock, + monitorService: monitorSvcMock, + }, &clientMocks{ + httpClientMock: httpClientMock, + tenantManagerMock: tntManagerMock, + monitorServiceMock: monitorSvcMock, + } +} + +type clientMocks struct { + httpClientMock *httpclientMocks.HttpClientMock + tenantManagerMock *tenant.TenantManagerMock + monitorServiceMock *monitorMocks.MockMonitorService } diff --git a/internal/circle/service.go b/internal/circle/service.go index 71ac39318..46c790c6a 100644 --- a/internal/circle/service.go +++ b/internal/circle/service.go @@ -6,6 +6,7 @@ import ( "github.com/stellar/go/strkey" + "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" "github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant" ) @@ -16,6 +17,7 @@ type Service struct { NetworkType utils.NetworkType EncryptionPassphrase string TenantManager tenant.ManagerInterface + MonitorService monitor.MonitorServiceInterface } const StellarChainCode = "XLM" @@ -36,6 +38,7 @@ type ServiceOptions struct { TenantManager tenant.ManagerInterface NetworkType utils.NetworkType EncryptionPassphrase string + MonitorService monitor.MonitorServiceInterface } func (o ServiceOptions) Validate() error { @@ -51,6 +54,10 @@ func (o ServiceOptions) Validate() error { return fmt.Errorf("TenantManager is required") } + if o.MonitorService == nil { + return fmt.Errorf("MonitorService is required") + } + err := o.NetworkType.Validate() if err != nil { return fmt.Errorf("validating NetworkType: %w", err) @@ -75,6 +82,7 @@ func NewService(opts ServiceOptions) (*Service, error) { NetworkType: opts.NetworkType, EncryptionPassphrase: opts.EncryptionPassphrase, TenantManager: opts.TenantManager, + MonitorService: opts.MonitorService, }, nil } @@ -111,7 +119,12 @@ func (s *Service) getClientForTenantInContext(ctx context.Context) (ClientInterf if err != nil { return nil, fmt.Errorf("retrieving decrypted Circle API key: %w", err) } - return s.ClientFactory(s.NetworkType, apiKey, s.TenantManager), nil + return s.ClientFactory(ClientOptions{ + APIKey: apiKey, + NetworkType: s.NetworkType, + TenantManager: s.TenantManager, + MonitorService: s.MonitorService, + }), nil } func (s *Service) Ping(ctx context.Context) (bool, error) { diff --git a/internal/circle/service_test.go b/internal/circle/service_test.go index 02a27160a..552772a2d 100644 --- a/internal/circle/service_test.go +++ b/internal/circle/service_test.go @@ -9,16 +9,18 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/db" "github.com/stellar/stellar-disbursement-platform-backend/db/dbtest" + monitorMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor/mocks" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" "github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant" ) func Test_ServiceOptions_Validate(t *testing.T) { - var clientFactory ClientFactory = func(networkType utils.NetworkType, apiKey string, tntManager tenant.ManagerInterface) ClientInterface { + var clientFactory ClientFactory = func(clientOpts ClientOptions) ClientInterface { return nil } circleClientConfigModel := &ClientConfigModel{} mockTenantManager := &tenant.TenantManagerMock{} + mockMonitorSvc := monitorMocks.NewMockMonitorService(t) testCases := []struct { name string @@ -40,12 +42,18 @@ func Test_ServiceOptions_Validate(t *testing.T) { opts: ServiceOptions{ClientFactory: clientFactory, ClientConfigModel: circleClientConfigModel}, expectedErrContains: "TenantManager is required", }, + { + name: "MonitorService validation fails", + opts: ServiceOptions{ClientFactory: clientFactory, ClientConfigModel: circleClientConfigModel, TenantManager: mockTenantManager}, + expectedErrContains: "MonitorService is required", + }, { name: "NetworkType validation fails", opts: ServiceOptions{ ClientFactory: clientFactory, ClientConfigModel: circleClientConfigModel, TenantManager: mockTenantManager, + MonitorService: mockMonitorSvc, NetworkType: utils.NetworkType("FOOBAR"), }, expectedErrContains: `validating NetworkType: invalid network type "FOOBAR"`, @@ -56,6 +64,7 @@ func Test_ServiceOptions_Validate(t *testing.T) { ClientFactory: clientFactory, ClientConfigModel: circleClientConfigModel, TenantManager: mockTenantManager, + MonitorService: mockMonitorSvc, NetworkType: utils.TestnetNetworkType, EncryptionPassphrase: "FOO BAR", }, @@ -68,6 +77,7 @@ func Test_ServiceOptions_Validate(t *testing.T) { ClientConfigModel: circleClientConfigModel, TenantManager: mockTenantManager, NetworkType: utils.TestnetNetworkType, + MonitorService: mockMonitorSvc, EncryptionPassphrase: "SCW5I426WV3IDTLSTLQEHC6BMXWI2Z6C4DXAOC4ZA2EIHTAZQ6VD3JI6", }, }, @@ -93,11 +103,12 @@ func Test_NewService(t *testing.T) { }) t.Run("๐ŸŽ‰ successfully creates a new Service", func(t *testing.T) { - clientFactory := func(networkType utils.NetworkType, apiKey string, tntManager tenant.ManagerInterface) ClientInterface { + clientFactory := func(clientOptions ClientOptions) ClientInterface { return nil } clientConfigModel := &ClientConfigModel{} mockTntManager := &tenant.TenantManagerMock{} + mockMonitorSvc := monitorMocks.NewMockMonitorService(t) networkType := utils.TestnetNetworkType encryptionPassphrase := "SCW5I426WV3IDTLSTLQEHC6BMXWI2Z6C4DXAOC4ZA2EIHTAZQ6VD3JI6" @@ -107,6 +118,7 @@ func Test_NewService(t *testing.T) { TenantManager: mockTntManager, NetworkType: networkType, EncryptionPassphrase: encryptionPassphrase, + MonitorService: mockMonitorSvc, }) assert.NoError(t, err) @@ -117,7 +129,18 @@ func Test_NewService(t *testing.T) { NetworkType: networkType, EncryptionPassphrase: encryptionPassphrase, } - assert.Equal(t, wantService.ClientFactory(networkType, "FOO BAR", mockTntManager), svc.ClientFactory(networkType, "FOO BAR", mockTntManager)) + + assert.Equal(t, wantService.ClientFactory(ClientOptions{ + NetworkType: networkType, + APIKey: "FOO BAR", + TenantManager: mockTntManager, + MonitorService: mockMonitorSvc, + }), svc.ClientFactory(ClientOptions{ + NetworkType: networkType, + APIKey: "FOO BAR", + TenantManager: mockTntManager, + MonitorService: mockMonitorSvc, + })) assert.Equal(t, wantService.ClientConfigModel, svc.ClientConfigModel) assert.Equal(t, wantService.NetworkType, svc.NetworkType) assert.Equal(t, wantService.EncryptionPassphrase, svc.EncryptionPassphrase) @@ -140,6 +163,7 @@ func Test_Service_getClient(t *testing.T) { networkType := utils.TestnetNetworkType clientConfigModel := NewClientConfigModel(dbConnectionPool) mockTntManager := &tenant.TenantManagerMock{} + mMonitorService := monitorMocks.NewMockMonitorService(t) // Add a client config to the database. err = clientConfigModel.Upsert(ctx, ClientConfigUpdate{ @@ -156,12 +180,18 @@ func Test_Service_getClient(t *testing.T) { TenantManager: mockTntManager, NetworkType: networkType, EncryptionPassphrase: encryptionPassphrase, + MonitorService: mMonitorService, }) assert.NoError(t, err) circleClient, err := svc.getClientForTenantInContext(ctx) assert.NoError(t, err) - wantCircleClient := NewClient(networkType, apiKey, &tenant.TenantManagerMock{}) + wantCircleClient := NewClient(ClientOptions{ + NetworkType: networkType, + APIKey: apiKey, + TenantManager: mockTntManager, + MonitorService: mMonitorService, + }) assert.Equal(t, wantCircleClient, circleClient) } @@ -192,13 +222,14 @@ func Test_Service_allMethods(t *testing.T) { // Method used to spin up a service with a mock client. createService := func(t *testing.T, mCircleClient *MockClient) *Service { svc, err := NewService(ServiceOptions{ - ClientFactory: func(networkType utils.NetworkType, apiKey string, tntManager tenant.ManagerInterface) ClientInterface { + ClientFactory: func(clientOpts ClientOptions) ClientInterface { return mCircleClient }, ClientConfigModel: clientConfigModel, TenantManager: mockTntManager, NetworkType: networkType, EncryptionPassphrase: encryptionPassphrase, + MonitorService: monitorMocks.NewMockMonitorService(t), }) require.NoError(t, err) return svc diff --git a/internal/dependencyinjection/circle_service_test.go b/internal/dependencyinjection/circle_service_test.go index e020b609c..1b1b43368 100644 --- a/internal/dependencyinjection/circle_service_test.go +++ b/internal/dependencyinjection/circle_service_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/stellar-disbursement-platform-backend/internal/circle" + monitorMocks "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor/mocks" "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" "github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant" ) @@ -21,6 +22,7 @@ func Test_NewCircleService(t *testing.T) { TenantManager: &tenant.TenantManagerMock{}, NetworkType: utils.TestnetNetworkType, EncryptionPassphrase: keypair.MustRandom().Seed(), + MonitorService: monitorMocks.NewMockMonitorService(t), } t.Run("should create and return the same instance on the second call", func(t *testing.T) { diff --git a/internal/monitor/main.go b/internal/monitor/main.go index 2df87c145..a79f8ce20 100644 --- a/internal/monitor/main.go +++ b/internal/monitor/main.go @@ -34,7 +34,7 @@ type MetricOptions struct { func GetClient(opts MetricOptions) (MonitorClient, error) { switch opts.MetricType { case MetricTypePrometheus: - return NewPrometheusClient() + return newPrometheusClient() case MetricTypeTSSPrometheus: return NewTSSPrometheusClient() default: diff --git a/internal/monitor/metric_tags.go b/internal/monitor/metric_tags.go index 4a71dca93..9d7ff2b6b 100644 --- a/internal/monitor/metric_tags.go +++ b/internal/monitor/metric_tags.go @@ -11,6 +11,9 @@ const ( // AnchorPlatformAuthProtection AnchorPlatformAuthProtectionEnsuredCounterTag MetricTag = "anchor_platform_auth_protection_ensured_counter" AnchorPlatformAuthProtectionMissingCounterTag MetricTag = "anchor_platform_auth_protection_missing_counter" + // Circle API Requests + CircleAPIRequestDurationTag MetricTag = "circle_api_request_duration_seconds" + CircleAPIRequestsTotalTag MetricTag = "circle_api_requests_total" ) func (m MetricTag) ListAll() []MetricTag { @@ -21,5 +24,7 @@ func (m MetricTag) ListAll() []MetricTag { DisbursementsCounterTag, AnchorPlatformAuthProtectionEnsuredCounterTag, AnchorPlatformAuthProtectionMissingCounterTag, + CircleAPIRequestDurationTag, + CircleAPIRequestsTotalTag, } } diff --git a/internal/monitor/monitor_labels.go b/internal/monitor/monitor_labels.go index 32f7c1f3d..f34cb775e 100644 --- a/internal/monitor/monitor_labels.go +++ b/internal/monitor/monitor_labels.go @@ -23,3 +23,23 @@ func (d DisbursementLabels) ToMap() map[string]string { "wallet": d.Wallet, } } + +type CircleLabels struct { + Method string + Endpoint string + Status string + StatusCode string + TenantName string +} + +func (c CircleLabels) ToMap() map[string]string { + return map[string]string{ + "method": c.Method, + "endpoint": c.Endpoint, + "status": c.Status, + "status_code": c.StatusCode, + "tenant_name": c.TenantName, + } +} + +var CircleLabelNames = []string{"method", "endpoint", "status", "status_code", "tenant_name"} diff --git a/internal/monitor/prometheus_client.go b/internal/monitor/prometheus_client.go index 9cca6260d..a7847f8aa 100644 --- a/internal/monitor/prometheus_client.go +++ b/internal/monitor/prometheus_client.go @@ -1,11 +1,11 @@ package monitor import ( - "fmt" "net/http" "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stellar/go/support/log" ) @@ -14,7 +14,7 @@ type prometheusClient struct { httpHandler http.Handler } -func (prometheusClient) GetMetricType() MetricType { +func (p *prometheusClient) GetMetricType() MetricType { return MetricTypePrometheus } @@ -63,21 +63,16 @@ func (p *prometheusClient) MonitorHistogram(value float64, tag MetricTag, labels histogram.With(labels).Observe(value) } -func NewPrometheusClient() (*prometheusClient, error) { +func newPrometheusClient() (*prometheusClient, error) { // register Prometheus metrics metricsRegistry := prometheus.NewRegistry() - var metricTag MetricTag - for _, tag := range metricTag.ListAll() { - if summaryVecMetric, ok := SummaryVecMetrics[tag]; ok { - metricsRegistry.MustRegister(summaryVecMetric) - } else if counterMetric, ok := CounterMetrics[tag]; ok { - metricsRegistry.MustRegister(counterMetric) - } else if counterVecMetric, ok := CounterVecMetrics[tag]; ok { - metricsRegistry.MustRegister(counterVecMetric) - } else { - return nil, fmt.Errorf("metric not registered in prometheus metrics: %s", tag) - } + // register default Prometheus metrics + metricsRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + metricsRegistry.MustRegister(collectors.NewGoCollector()) + + for _, metric := range PrometheusMetrics() { + metricsRegistry.MustRegister(metric) } return &prometheusClient{httpHandler: promhttp.HandlerFor(metricsRegistry, promhttp.HandlerOpts{})}, nil diff --git a/internal/monitor/prometheus_client_test.go b/internal/monitor/prometheus_client_test.go index eaf7a2b84..c475fcec2 100644 --- a/internal/monitor/prometheus_client_test.go +++ b/internal/monitor/prometheus_client_test.go @@ -185,7 +185,7 @@ func Test_PrometheusClient_MonitorCounters(t *testing.T) { assert.NotEmpty(t, data) body := string(data) - metric := `sdp_bussiness_disbursements_counter{asset="USDC",country="UKR",wallet="Mock Wallet"} 1` + metric := `sdp_business_disbursements_counter{asset="USDC",country="UKR",wallet="Mock Wallet"} 1` assert.Contains(t, body, metric) diff --git a/internal/monitor/prometheus_metrics.go b/internal/monitor/prometheus_metrics.go index 83c482c56..91cac7220 100644 --- a/internal/monitor/prometheus_metrics.go +++ b/internal/monitor/prometheus_metrics.go @@ -1,6 +1,30 @@ package monitor -import "github.com/prometheus/client_golang/prometheus" +import ( + "github.com/prometheus/client_golang/prometheus" +) + +func PrometheusMetrics() map[MetricTag]prometheus.Collector { + metrics := make(map[MetricTag]prometheus.Collector) + + for tag, summaryVec := range SummaryVecMetrics { + metrics[tag] = summaryVec + } + + for tag, counter := range CounterMetrics { + metrics[tag] = counter + } + + for tag, histogramVec := range HistogramVecMetrics { + metrics[tag] = histogramVec + } + + for tag, counterVec := range CounterVecMetrics { + metrics[tag] = counterVec + } + + return metrics +} var SummaryVecMetrics = map[MetricTag]*prometheus.SummaryVec{ HttpRequestDurationTag: prometheus.NewSummaryVec(prometheus.SummaryOpts{ @@ -34,13 +58,26 @@ var CounterMetrics = map[MetricTag]prometheus.Counter{ }), } -var HistogramVecMetrics map[MetricTag]prometheus.HistogramVec +var HistogramVecMetrics = map[MetricTag]*prometheus.HistogramVec{ + CircleAPIRequestDurationTag: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "sdp", Subsystem: "circle", Name: string(CircleAPIRequestDurationTag), + Help: "A histogram of the Circle API request durations", + }, + CircleLabelNames, + ), +} var CounterVecMetrics = map[MetricTag]*prometheus.CounterVec{ DisbursementsCounterTag: prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: "sdp", Subsystem: "bussiness", Name: string(DisbursementsCounterTag), + Namespace: "sdp", Subsystem: "business", Name: string(DisbursementsCounterTag), Help: "Disbursements Counter", }, []string{"asset", "country", "wallet"}, ), + CircleAPIRequestsTotalTag: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "sdp", Subsystem: "circle", Name: string(CircleAPIRequestsTotalTag), + Help: "A counter of the Circle API requests", + }, + CircleLabelNames, + ), } diff --git a/internal/monitor/utils.go b/internal/monitor/utils.go new file mode 100644 index 000000000..bd4e34058 --- /dev/null +++ b/internal/monitor/utils.go @@ -0,0 +1,19 @@ +package monitor + +import ( + "fmt" + "net/http" +) + +const ( + noHTTPStatus = "0" + successStatus = "success" + errorStatus = "error" +) + +func ParseHTTPResponseStatus(resp *http.Response, reqErr error) (status, statusCode string) { + if reqErr != nil { + return errorStatus, noHTTPStatus + } + return successStatus, fmt.Sprint(resp.StatusCode) +} diff --git a/internal/serve/httphandler/circle_config_handler.go b/internal/serve/httphandler/circle_config_handler.go index 1aad847e6..0c50507fc 100644 --- a/internal/serve/httphandler/circle_config_handler.go +++ b/internal/serve/httphandler/circle_config_handler.go @@ -10,6 +10,7 @@ import ( "github.com/stellar/go/support/render/httpjson" "github.com/stellar/stellar-disbursement-platform-backend/internal/circle" + "github.com/stellar/stellar-disbursement-platform-backend/internal/monitor" "github.com/stellar/stellar-disbursement-platform-backend/internal/serve/httperror" "github.com/stellar/stellar-disbursement-platform-backend/internal/transactionsubmission/engine/signing" sdpUtils "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" @@ -26,6 +27,7 @@ type CircleConfigHandler struct { EncryptionPassphrase string CircleClientConfigModel circle.ClientConfigModelInterface DistributionAccountResolver signing.DistributionAccountResolver + MonitorService monitor.MonitorServiceInterface } type PatchCircleConfigRequest struct { @@ -160,7 +162,12 @@ func (h CircleConfigHandler) validateConfigWithCircle(ctx context.Context, patch } } - circleClient := h.CircleFactory(h.NetworkType, apiKey, h.TenantManager) + circleClient := h.CircleFactory(circle.ClientOptions{ + NetworkType: h.NetworkType, + APIKey: apiKey, + TenantManager: h.TenantManager, + MonitorService: h.MonitorService, + }) // validate incoming APIKey if patchRequest.APIKey != nil { diff --git a/internal/serve/httphandler/circle_config_handler_test.go b/internal/serve/httphandler/circle_config_handler_test.go index 7555ddad6..231c96634 100644 --- a/internal/serve/httphandler/circle_config_handler_test.go +++ b/internal/serve/httphandler/circle_config_handler_test.go @@ -194,7 +194,7 @@ func TestCircleConfigHandler_Patch(t *testing.T) { tc.prepareMocksFn(t, mDistributionAccountResolver, mCircleClient, mTenantManager) handler.DistributionAccountResolver = mDistributionAccountResolver - handler.CircleFactory = func(networkType utils.NetworkType, apiKey string, tntManager tenant.ManagerInterface) circle.ClientInterface { + handler.CircleFactory = func(clientOpts circle.ClientOptions) circle.ClientInterface { return mCircleClient } handler.TenantManager = mTenantManager @@ -366,7 +366,7 @@ func Test_CircleConfigHandler_validateConfigWithCircle(t *testing.T) { handler.Encrypter = mEncrypter handler.CircleClientConfigModel = mCircleClientConfigModel - handler.CircleFactory = func(networkType utils.NetworkType, apiKey string, tntManager tenant.ManagerInterface) circle.ClientInterface { + handler.CircleFactory = func(clientOpts circle.ClientOptions) circle.ClientInterface { return mCircleClient } } diff --git a/internal/serve/serve.go b/internal/serve/serve.go index e9f13e404..06203ca93 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -396,6 +396,7 @@ func handleHTTP(o ServeOptions) *chi.Mux { EncryptionPassphrase: o.DistAccEncryptionPassphrase, CircleClientConfigModel: circle.NewClientConfigModel(o.MtnDBConnectionPool), DistributionAccountResolver: o.SubmitterEngine.DistributionAccountResolver, + MonitorService: o.MonitorService, }.Patch) }) diff --git a/resources/grafana/dashboard.json b/resources/grafana/dashboard.json index dd0e91791..172275af8 100644 --- a/resources/grafana/dashboard.json +++ b/resources/grafana/dashboard.json @@ -39,10 +39,7 @@ "id": 2, "panels": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Rate request per second in a 5 min interval. Filtered by route, method, status and instance.", "fieldConfig": { "defaults": { @@ -140,10 +137,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "rate(sdp_http_requests_duration_seconds_count{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"}[5m])", "legendFormat": "{{method}} {{route}}: {{status}} {{instance}}", @@ -155,10 +149,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average request time duration in a 5 min interval. Filtered by route, method, status and instance.", "fieldConfig": { "defaults": { @@ -229,10 +220,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * (rate(sdp_http_requests_duration_seconds_sum{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"}[5m]) / rate(sdp_http_requests_duration_seconds_count{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"}[5m]))", "legendFormat": "{{method}} {{route}}: {{status}} {{instance}}", @@ -244,10 +232,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average rate request per second in a 5 min interval aggregate by instance. Filtered by route, method, status.", "fieldConfig": { "defaults": { @@ -320,10 +305,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "avg(rate(sdp_http_requests_duration_seconds_count{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"}[5m])) by (route, method, status)", "legendFormat": "{{method}} {{route}}: {{status}}", @@ -335,10 +317,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average request time duration in a 5 min interval aggregate by instances. Filtered by route, method, status.", "fieldConfig": { "defaults": { @@ -409,10 +388,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * avg((rate(sdp_http_requests_duration_seconds_sum{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"}[5m]) / rate(sdp_http_requests_duration_seconds_count{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"}[5m]))) by (route, method, status)", "legendFormat": "{{method}} {{route}}: {{status}}", @@ -424,10 +400,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "fieldConfig": { "defaults": { "color": { @@ -523,10 +496,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * sdp_http_requests_duration_seconds{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"}", "legendFormat": "q{{quantile}} {{method}} {{route}}: {{status}} {{instance}}", @@ -538,10 +508,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Total number of requests filtered by route, method, status and instance.", "fieldConfig": { "defaults": { @@ -584,10 +551,7 @@ "pluginVersion": "9.3.8", "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "sum(sdp_http_requests_duration_seconds_count{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"})", "legendFormat": "__auto", @@ -599,10 +563,7 @@ "type": "stat" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Total average time duration from a request. Filtered by route, method, status and instance", "fieldConfig": { "defaults": { @@ -645,10 +606,7 @@ "pluginVersion": "9.3.8", "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * avg(sdp_http_requests_duration_seconds_sum{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"} / sdp_http_requests_duration_seconds_count{route=~\"$route\", method=~\"$method\", status=~\"$status\", instance=~\"$instance\"})", "legendFormat": "__auto", @@ -674,10 +632,7 @@ "id": 14, "panels": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Rate successful query per second in a 5 min interval. Filtered by query_type and instance.", "fieldConfig": { "defaults": { @@ -751,10 +706,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "rate(sdp_db_successful_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m])", "legendFormat": "{{query_type}} {{instance}}", @@ -766,10 +718,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average successful query duration in a 5 min interval. Filtered by query_type and instance.", "fieldConfig": { "defaults": { @@ -843,10 +792,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * (rate(sdp_db_successful_queries_duration_sum{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]) / rate(sdp_db_successful_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]))", "legendFormat": "{{query_type}} {{instance}}", @@ -858,10 +804,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average rate successful query per second in a 5 min interval. Filtered by query_type.", "fieldConfig": { "defaults": { @@ -935,10 +878,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "avg(rate(sdp_db_successful_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m])) by (query_type)", "legendFormat": "{{query_type}}", @@ -950,10 +890,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average successful query duration in a 5 min interval aggregate by instances. Filtered by query_type.", "fieldConfig": { "defaults": { @@ -1027,10 +964,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "avg(1000 * (rate(sdp_db_successful_queries_duration_sum{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]) / rate(sdp_db_successful_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]))) by (query_type)", "legendFormat": "{{query_type}}", @@ -1042,10 +976,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "fieldConfig": { "defaults": { "color": { @@ -1119,10 +1050,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * sdp_db_successful_queries_duration{query_type=~\"$query_type\", instance=~\"$instance\"}", "legendFormat": "q{{quantile}} {{query_type}} {{instance}}", @@ -1134,10 +1062,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Rate failure query per second in a 5 min interval. Filtered by query_type and instance.", "fieldConfig": { "defaults": { @@ -1210,10 +1135,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "rate(sdp_db_failure_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m])", "legendFormat": "{{query_type}} {{instance}}", @@ -1225,10 +1147,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average failure query duration in a 5 min interval. Filtered by query_type and instance.", "fieldConfig": { "defaults": { @@ -1301,10 +1220,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * (rate(sdp_db_failure_queries_duration_sum{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]) / rate(sdp_db_failure_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]))", "legendFormat": "{{query_type}} {{instance}}", @@ -1316,10 +1232,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average rate failure query per second in a 5 min interval. Filtered by query_type.", "fieldConfig": { "defaults": { @@ -1392,10 +1305,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "avg(rate(sdp_db_failure_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m])) by (query_type)", "legendFormat": "{{query_type}}", @@ -1407,10 +1317,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "description": "Average failure query duration in a 5 min interval aggregate by instances. Filtered by query_type.", "fieldConfig": { "defaults": { @@ -1483,10 +1390,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "avg(1000 * (rate(sdp_db_failure_queries_duration_sum{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]) / rate(sdp_db_failure_queries_duration_count{query_type=~\"$query_type\", instance=~\"$instance\"}[5m]))) by (query_type)", "legendFormat": "{{query_type}}", @@ -1498,10 +1402,7 @@ "type": "timeseries" }, { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "fieldConfig": { "defaults": { "color": { @@ -1575,10 +1476,7 @@ }, "targets": [ { - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "editorMode": "code", "expr": "1000 * sdp_db_failure_queries_duration{query_type=~\"$query_type\", instance=~\"$instance\"}", "legendFormat": "q{{quantile}} {{query_type}} {{instance}}", @@ -1592,6 +1490,361 @@ ], "title": "DB Metrics", "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 2 + }, + "id": 28, + "panels": [ { + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 2 + }, + "id": 30, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.3", + "targets": [ + { + "datasource": "prometheus", + "editorMode": "code", + "expr": "100 * (sum(sdp_circle_circle_api_requests_total{status=\"error\"}) \n/\nsum(sdp_circle_circle_api_requests_total)\nor vector(0))", + "interval": "", + "legendFormat": "Total Requests", + "range": true, + "refId": "A" + } + ], + "title": "Circle API - Error Rate", + "type": "stat" + }, + { + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 2 + }, + "id": 32, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.3", + "targets": [ + { + "expr": "sum(sdp_circle_circle_api_requests_total)", + "legendFormat": "Total Requests", + "refId": "A" + } + ], + "title": "Circle API - Request Count", + "type": "stat" + }, + { + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 70 + }, + { + "color": "red", + "value": 85 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 33, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.1.3", + "targets": [ + { + "datasource": "prometheus", + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(0.95, sum by (le, endpoint, method) (rate(sdp_circle_circle_api_request_duration_seconds_bucket[1m]) or vector(0)) ) ", + "format": "time_series", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Circle API - 95th Percentile Request Duration by Endpoint and Method", + "type": "gauge" + }, + { + "datasource": "prometheus", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 31, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "show": true, + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": "prometheus", + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(sdp_circle_circle_api_request_duration_seconds_sum[5m])) by (endpoint,method) / sum(rate(sdp_circle_circle_api_request_duration_seconds_count[5m])) by (endpoint,method)", + "format": "time_series", + "instant": false, + "legendFormat": "{{method, endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Circle API - Average Request Duration by Method and Endpoint", + "type": "timeseries" + }, + { + "datasource": "prometheus", + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 0, + "stacking": { + "group": "A", + "mode": "none" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 29, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "show": true, + "showLegend": true + }, + "tooltip": { + "show": true, + "showHistogram": true + } + }, + "targets": [ + { + "datasource": "prometheus", + "editorMode": "code", + "expr": "sum by (le, endpoint, method) (rate(sdp_circle_circle_api_request_duration_seconds_bucket[1m]))", + "format": "heatmap", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Circle API - Histogram of Request Durations", + "type": "histogram" + }], + "title": "Cicle Metrics", + "type": "row" } ], "schemaVersion": 37, @@ -1605,10 +1858,7 @@ "text": "All", "value": "$__all" }, - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "definition": "query_result(sdp_http_requests_duration_seconds_count)", "description": "", "hide": 0, @@ -1636,10 +1886,7 @@ "GET" ] }, - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "definition": "query_result(sdp_http_requests_duration_seconds_count)", "hide": 0, "includeAll": true, @@ -1666,10 +1913,7 @@ "200" ] }, - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "definition": "query_result(sdp_http_requests_duration_seconds_count)", "hide": 0, "includeAll": true, @@ -1692,10 +1936,7 @@ "text": "All", "value": "$__all" }, - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "definition": "query_result(sdp_http_requests_duration_seconds_count)", "hide": 0, "includeAll": true, @@ -1719,10 +1960,7 @@ "text": "All", "value": "$__all" }, - "datasource": { - "type": "prometheus", - "uid": "NJaP9W-4z" - }, + "datasource": "prometheus", "definition": "query_result(sdp_db_failure_queries_duration_count or sdp_db_successful_queries_duration_count)", "hide": 0, "includeAll": true, @@ -1749,7 +1987,6 @@ "timepicker": {}, "timezone": "", "title": "Dashboard SDPV2", - "uid": "XIR0jW-4k", "version": 40, "weekStart": "" } \ No newline at end of file From 457db686e29f37687a97395639d5d34ad6452343 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:17:39 +0000 Subject: [PATCH 18/75] Bump the minor-and-patch group with 2 updates (#403) --- go.list | 4 ++-- go.mod | 4 ++-- go.sum | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.list b/go.list index 661078a70..95c8bd5e7 100644 --- a/go.list +++ b/go.list @@ -69,7 +69,7 @@ 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 github.com/go-chi/chi/v5 v5.1.0 -github.com/go-chi/httprate v0.12.1 +github.com/go-chi/httprate v0.14.0 github.com/go-errors/errors v1.5.1 github.com/go-gorp/gorp/v3 v3.1.0 github.com/go-kit/log v0.2.1 @@ -201,7 +201,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.0 +github.com/prometheus/client_golang v1.20.2 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.55.0 github.com/prometheus/procfs v0.15.1 diff --git a/go.mod b/go.mod index c7e9c522c..d65b33301 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/getsentry/sentry-go v0.28.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.12.1 + github.com/go-chi/httprate v0.14.0 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 @@ -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.0 + github.com/prometheus/client_golang v1.20.2 github.com/rs/cors v1.11.0 github.com/rubenv/sql-migrate v1.7.0 github.com/segmentio/kafka-go v0.4.47 diff --git a/go.sum b/go.sum index d390a623a..ee0656e6a 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyN 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= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/httprate v0.12.1 h1:55l3IWrPcipqKb72yBzH+grF51z5w+2Bb/Qmu1bos/E= -github.com/go-chi/httprate v0.12.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= +github.com/go-chi/httprate v0.14.0 h1:c8szLJc+Gn+1EC1jjv3q88Om4a9USAqU9lL8wQFVX2M= +github.com/go-chi/httprate v0.14.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= @@ -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.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI= -github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/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= From 1375e2a26618985162908e66ba70b6d65d8ef0fa Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Sat, 31 Aug 2024 01:25:58 +0100 Subject: [PATCH 19/75] SDP-1321 fix asset handler test (#407) --- internal/serve/httphandler/assets_handler_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/serve/httphandler/assets_handler_test.go b/internal/serve/httphandler/assets_handler_test.go index 2b2669239..2e7402e39 100644 --- a/internal/serve/httphandler/assets_handler_test.go +++ b/internal/serve/httphandler/assets_handler_test.go @@ -1441,16 +1441,22 @@ func Test_AssetHandler_submitChangeTrustTransaction_makeSurePreconditionsAreSetA // matchPreconditionsTimeboundsFn is a function meant to be used with mock.MatchedBy to check that the preconditions are set as expected. assertExpectedPreconditionsWithTimeboundsTolerance := func(t *testing.T, expectedTx *txnbuild.Transaction, actualTxIndex int) func(args mock.Arguments) { + creationTime := time.Now() return func(args mock.Arguments) { - actualTx, ok := args.Get(int(actualTxIndex)).(*txnbuild.Transaction) + actualTx, ok := args.Get(actualTxIndex).(*txnbuild.Transaction) require.True(t, ok) expectedPreconditions := expectedTx.ToXDR().Preconditions() - expectedTime := time.Unix(int64(expectedPreconditions.TimeBounds.MaxTime), 0).UTC() actualPreconditions := actualTx.ToXDR().Preconditions() - actualTime := time.Unix(int64(actualPreconditions.TimeBounds.MaxTime), 0).UTC() - require.WithinDuration(t, expectedTime, actualTime, 5*time.Second) + require.Equal(t, expectedPreconditions.TimeBounds.MinTime, actualPreconditions.TimeBounds.MinTime) + + expectedMaxTime := time.Unix(int64(expectedPreconditions.TimeBounds.MaxTime), 0).UTC() + actualMaxTime := time.Unix(int64(actualPreconditions.TimeBounds.MaxTime), 0).UTC() + timeSinceCreation := time.Since(creationTime) + + expectedAdjustedMaxTime := expectedMaxTime.Add(timeSinceCreation) + require.WithinDuration(t, expectedAdjustedMaxTime, actualMaxTime, 5*time.Second) } } From 72ab3e6773dbd59859203d39e6ca8edca42d2673 Mon Sep 17 00:00:00 2001 From: Marcelo Salloum dos Santos Date: Thu, 5 Sep 2024 15:14:57 -0700 Subject: [PATCH 20/75] [SDP-1295] Add initial screen so receivers can select between email and phoneNumber (#406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What Add an initial screen so receivers can select between email and phone number. As part of it, I refactored the code to streamline it and rely on reusable methods rather than manipulating the HTML elements directly all the time. #### Screenshots: ##### New Initial Screen Screenshot 2024-08-30 at 4 02 18โ€ฏPM ##### Email Input Screen Screenshot 2024-08-30 at 4 02 32โ€ฏPM ### Why Address https://stellarorg.atlassian.net/browse/SDP-1295 --- .../htmltemplate/tmpl/receiver_register.tmpl | 241 +++--- .../httphandler/receiver_send_otp_handler.go | 1 + .../publicfiles/js/receiver_registration.js | 689 +++++++++--------- 3 files changed, 477 insertions(+), 454 deletions(-) diff --git a/internal/htmltemplate/tmpl/receiver_register.tmpl b/internal/htmltemplate/tmpl/receiver_register.tmpl index b92c4140b..328a0bf41 100644 --- a/internal/htmltemplate/tmpl/receiver_register.tmpl +++ b/internal/htmltemplate/tmpl/receiver_register.tmpl @@ -25,8 +25,71 @@
+ +
+
+

Select Verification Method

+ +

Please choose how you would like to receive your verification code.

+

โš ๏ธ Attention, it needs to match the form of contact where you received your invitation to receive this payment.

+ +
+
+ + +
+ +
+ +
+
+
+
+ + +
+
+

Enter your email address to get verified

+ +

+ Enter your email address below. If you are pre-approved, you will + receive a one-time passcode. +

+ +
+
+ + +
+ +
+ +
+ +
+
+
+
+ -
+

Enter your phone number to get verified

@@ -57,47 +120,14 @@
- - - @@ -117,7 +147,7 @@

-
+
- - - -
+ + + + + {{.JWTToken}}
diff --git a/internal/serve/httphandler/receiver_send_otp_handler.go b/internal/serve/httphandler/receiver_send_otp_handler.go index 7e08ee3ef..00e9a38c7 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler.go +++ b/internal/serve/httphandler/receiver_send_otp_handler.go @@ -44,6 +44,7 @@ type ReceiverSendOTPResponseBody struct { VerificationField data.VerificationField `json:"verification_field"` } +// FIXME! /wallet-registration/otp returns a JSON with the field named `verification_field` but /wallet-registration/verification expects the field to be named `verification_type`. This inconsistency should be fixed. func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/internal/serve/publicfiles/js/receiver_registration.js b/internal/serve/publicfiles/js/receiver_registration.js index 375e3bc4f..566ea90cc 100644 --- a/internal/serve/publicfiles/js/receiver_registration.js +++ b/internal/serve/publicfiles/js/receiver_registration.js @@ -1,411 +1,402 @@ +// ------------------------------ START: ENUMS ------------------------------ +const ContactMethods = Object.freeze({ + PHONE_NUMBER: "phone_number", + EMAIL: "email", +}); + +const CurrentSection = Object.freeze({ + SELECT_OTP_METHOD: "selectOtpMethod", // SECTION 1 + PHONE_NUMBER: "phoneNumber", // SECTION 2.1 (w/ phone_number) + EMAIL_ADDRESS: "emailAddress", // SECTION 2.2 (w/ email) + PASSCODE: "passcode", // SECTION 3 +}); + +const VerificationField = Object.freeze({ + DATE_OF_BIRTH: "DATE_OF_BIRTH", + YEAR_MONTH: "YEAR_MONTH", + NATIONAL_ID_NUMBER: "NATIONAL_ID_NUMBER", + PIN: "PIN", +}); +// ------------------------------ END: ENUMS ------------------------------ + + +// ------------------------------ START: GLOBAL VARIABLES AND METHODS ------------------------------ const WalletRegistration = { jwtToken: "", intlTelInput: null, - phoneNumberErrorEl: null, privacyPolicyLink: "", -}; + contactMethod: "", + currentSection: CurrentSection.SELECT_OTP_METHOD, + verificationField: "", + + setSection(section) { + this.currentSection = section; + switch (section) { + case CurrentSection.PHONE_NUMBER: + this.contactMethod = ContactMethods.PHONE_NUMBER; + break; + case CurrentSection.EMAIL_ADDRESS: + this.contactMethod = ContactMethods.EMAIL; + break; + } -function getJwtToken() { - const tokenEl = document.querySelector("[data-jwt-token]"); + Object.values(CurrentSection).forEach((s) => { + const sectionEl = document.querySelector(`[data-section='${s}']`); + if (sectionEl) sectionEl.style.display = s === section ? "flex" : "none"; + }); + }, + + errorNotificationEl() { + return document.querySelector("[data-section-error]"); + }, + + successNotificationEl() { + return document.querySelector("[data-section-success]"); + }, + + toggleErrorNotification(title, message, isVisible) { + const errorNotificationEl = this.errorNotificationEl(); + toggleNotification("error", { parentEl: errorNotificationEl, title, message, isVisible }); + }, + + toggleSuccessNotification(title, message, isVisible) { + const successNotificationEl = this.successNotificationEl(); + toggleNotification("success", { parentEl: successNotificationEl, title, message, isVisible }); + }, + + getRecaptchaToken() { + const tokenSelectorMap = { + [CurrentSection.EMAIL_ADDRESS]: "#g-recaptcha-response", + [CurrentSection.PHONE_NUMBER]: "#g-recaptcha-response-1", + [CurrentSection.PASSCODE]: "#g-recaptcha-response-2", + }; + + const recaptchaEl = document.querySelector(tokenSelectorMap[this.currentSection]); + return recaptchaEl?.value || ""; + }, + + getSectionEl() { + return document.querySelector(`[data-section='${this.currentSection}']`); + }, + + toggleButtonsEnabled(isEnabled) { + const sectionEl = this.getSectionEl(); + const buttonEls = sectionEl?.querySelectorAll("[data-button]"); + if (!buttonEls) return; + const t = window.setTimeout(() => { + buttonEls.forEach((b) => { + b.disabled = !isEnabled; + }); - if (tokenEl) { - return tokenEl.innerHTML; - } -} + clearTimeout(t); + }, isEnabled ? 1000 : 0); + }, -function getPrivacyPolicyLink() { - const linkEl = document.querySelector("[data-privacy-policy-link]"); + getContactValue() { + switch (this.contactMethod) { + case ContactMethods.PHONE_NUMBER: + return this.intlTelInput.getNumber().trim(); + case ContactMethods.EMAIL: + return document.querySelector("#email_address").value.trim(); + } + }, - if (linkEl) { - return linkEl.innerHTML; - } -} + validateContactValue() { + const contactValue = this.getContactValue(); + if (!contactValue) { + this.toggleErrorNotification("Error", "Contact information is required", true); + return -1; + } -document.addEventListener("DOMContentLoaded", function () { - const footer = document.getElementById("WalletRegistration__PrivacyPolicy"); + if (this.contactMethod === ContactMethods.PHONE_NUMBER) { + if (!this.intlTelInput.isPossibleNumber()) { + this.toggleErrorNotification("Error", "Entered phone number is not valid", true); + return -1; + } + } else if (this.contactMethod === ContactMethods.EMAIL) { + const isValidEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + if (!isValidEmail(contactValue)) { + this.toggleErrorNotification("Error", "Entered email is not valid", true); + return -1; + } + } - if (WalletRegistration.privacyPolicyLink == "") { - footer.style = "display: none" + this.toggleErrorNotification("", "", false); + return 0; } -}); - -function toggleNotification(type, { parentEl, title, message, isVisible }) { - const titleEl = parentEl.querySelector(`[data-section-${type}-title]`); - const messageEl = parentEl.querySelector(`[data-section-${type}-message`); +}; +// ------------------------------ END: GLOBAL VARIABLES AND METHODS ------------------------------ - if (titleEl && messageEl) { - if (isVisible) { - parentEl.style.display = "flex"; - titleEl.innerHTML = title; - messageEl.innerHTML = message; - } else { - parentEl.style.display = "none"; - titleEl.innerHTML = ""; - messageEl.innerHTML = ""; - } - } -} -function toggleErrorNotification(parentEl, title, message, isVisible) { - toggleNotification("error", { parentEl, title, message, isVisible }); -} +// ------------------------------ START: INITIALIZATION ------------------------------ +window.onload = () => { + WalletRegistration.jwtToken = document.querySelector("[data-jwt-token]")?.innerHTML || ""; + WalletRegistration.privacyPolicyLink = document.querySelector("[data-privacy-policy-link]")?.innerHTML || ""; + WalletRegistration.intlTelInput = phoneNumberInit(); +}; -function toggleSuccessNotification(parentEl, title, message, isVisible) { - toggleNotification("success", { parentEl, title, message, isVisible }); -} +// Phone number input (ref: https://github.com/jackocnr/intl-tel-input) +function phoneNumberInit() { + const phoneNumberInput = document.querySelector("#phone_number"); -async function sendSms(phoneNumber, reCAPTCHAToken, onSuccess, onError) { - if (phoneNumber && reCAPTCHAToken) { - try { - const response = await fetch("/wallet-registration/otp", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${WalletRegistration.jwtToken}`, - }, - body: JSON.stringify({ - phone_number: phoneNumber, - recaptcha_token: reCAPTCHAToken, - }), - }); + const intlTelInput = window.intlTelInput(phoneNumberInput, { + utilsScript: "/static/js/intl-tel-input-v18.2.1-utils.min.js", + separateDialCode: true, + preferredCountries: [], + // Excluding Cuba, Iran, North Korea, and Syria + excludeCountries: ["cu", "ir", "kp", "sy"], + // Setting default country based on user's IP address + initialCountry: "auto", + geoIpLookup: (callback) => { + fetch("https://ipapi.co/json") + .then((res) => res.json()) + .then((data) => callback(data.country_code)) + .catch(() => callback("")); + }, + }); - const data = await response.json(); - if (!response.ok) { - throw new Error(data.error || "Something went wrong, please try again later."); + // Clear phone number error message + const errorNotificationEl = WalletRegistration.errorNotificationEl(); + ["change", "keyup"].forEach((event) => { + phoneNumberInput.addEventListener(event, () => { + if (errorNotificationEl.style.display !== "none") { + WalletRegistration.toggleErrorNotification("", "", false); + WalletRegistration.toggleButtonsEnabled(true); } - - onSuccess(data.verification_field);; - } catch (error) { - onError(error); - } - } + }); + }); + + return intlTelInput; } -function disableButtons(buttons) { - buttons.forEach((b) => { - b.disabled = true; +document.addEventListener("DOMContentLoaded", function () { + // Hide Privacy Policy Link if not provided + const footer = document.getElementById("WalletRegistration__PrivacyPolicy"); + if (!WalletRegistration.privacyPolicyLink) { + footer.style.display = "none"; + } + + // SECTION 1: Setup OTP Method Form + const otpMethodForm = document.getElementById("selectOtpMethodForm"); + otpMethodForm?.addEventListener("change", () => { + WalletRegistration.toggleErrorNotification("", "", false); + }); + otpMethodForm?.addEventListener("submit", (event) => { + event.preventDefault(); + handleOtpSelected(); }); -} -function enableButtons(buttons) { - const t = window.setTimeout(() => { - buttons.forEach((b) => { - b.disabled = false; + // SECTION 2: Setup Email and Phone Number Forms + ["submitEmailForm", "submitPhoneNumberForm"].forEach((formId) => { + document.getElementById(formId)?.addEventListener("submit", (event) => { + event.preventDefault(); + handleContactInfoSubmitted(); }); + }); - clearTimeout(t); - }, 1000); -} - -document.addEventListener("DOMContentLoaded", function () { - const form = document.getElementById("submitPhoneNumberForm"); + // SECTION 3: Setup OTP Form + document.getElementById("submitVerificationForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + handleVerificationInfoSubmitted(); + }); - form.addEventListener("submit", function (event) { - submitPhoneNumber(event); + // SECTION 3: Setup Resend OTP Button + document.getElementById("resendOtpButton")?.addEventListener("click", (event) => { + event.preventDefault(); + handleResendOtpClicked(); }); }); -async function submitPhoneNumber(event) { - event.preventDefault(); - const phoneNumberEl = document.querySelector("#phone_number"); - const phoneNumberSectionEl = document.querySelector( - "[data-section='phoneNumber']" - ); - const passcodeSectionEl = document.querySelector("[data-section='passcode']"); - const errorNotificationEl = WalletRegistration.phoneNumberErrorEl; - const reCAPTCHATokenEl = phoneNumberSectionEl.querySelector( - "#g-recaptcha-response" - ); - const buttonEls = phoneNumberSectionEl.querySelectorAll("[data-button]"); - const verificationFieldTitle = document.querySelector("label[for='verification']"); - const verificationFieldInput = document.querySelector("#verification"); - - if (!reCAPTCHATokenEl || !reCAPTCHATokenEl.value) { - toggleErrorNotification( - errorNotificationEl, - "Error", - "reCAPTCHA is required", - true - ); + +// ------------------------------ START: SECTION 1 ------------------------------ +function handleOtpSelected() { + const selectedMethod = document.querySelector('input[name="otp_method"]:checked')?.value; + if (!selectedMethod) { + WalletRegistration.toggleErrorNotification("Error", "Please select a contact method to receive your OTP", true); return; } + WalletRegistration.setSection(selectedMethod); +} +// ------------------------------ END: SECTION 1 ------------------------------ - toggleErrorNotification(errorNotificationEl, "", "", false); - - if ( - WalletRegistration.intlTelInput && - reCAPTCHATokenEl && - phoneNumberSectionEl && - passcodeSectionEl && - errorNotificationEl - ) { - disableButtons(buttonEls); - const phoneNumber = WalletRegistration.intlTelInput.getNumber(); - const reCAPTCHAToken = reCAPTCHATokenEl.value; - - if ( - phoneNumberEl.value.trim() && - !WalletRegistration.intlTelInput.isPossibleNumber() - ) { - toggleErrorNotification( - errorNotificationEl, - "Error", - "Entered phone number is not valid", - true - ); - return; - } - function showNextPage(verificationField) { - verificationFieldInput.type = "text"; - if(verificationField === "DATE_OF_BIRTH") { - verificationFieldTitle.textContent = "Date of birth"; - verificationFieldInput.name = "date_of_birth"; - verificationFieldInput.type = "date"; - } - else if(verificationField === "YEAR_MONTH") { - verificationFieldTitle.textContent = "Date of birth (Year/Month)"; - verificationFieldInput.name = "year_month"; - verificationFieldInput.type = "month"; - } - else if(verificationField === "NATIONAL_ID_NUMBER") { - verificationFieldTitle.textContent = "National ID number"; - verificationFieldInput.name = "national_id_number"; - } - else if(verificationField === "PIN") { - verificationFieldTitle.textContent = "Pin"; - verificationFieldInput.name = "pin"; - } +// ------------------------------ START: SECTION 2 ------------------------------ +async function handleContactInfoSubmitted() { + if (![CurrentSection.PHONE_NUMBER, CurrentSection.EMAIL_ADDRESS].includes(WalletRegistration.currentSection)) { + alert("Invalid section to submit contact information: " + WalletRegistration.currentSection); + return; + } - phoneNumberSectionEl.style.display = "none"; - reCAPTCHATokenEl.style.display = "none"; - passcodeSectionEl.style.display = "flex"; - enableButtons(buttonEls); - } + const reCAPTCHAToken = WalletRegistration.getRecaptchaToken(); + if (!reCAPTCHAToken) { + WalletRegistration.toggleErrorNotification("Error", "reCAPTCHA is required", true); + return; + } - function showErrorMessage(error) { - toggleErrorNotification(errorNotificationEl, "Error", error, true); - enableButtons(buttonEls); + WalletRegistration.toggleErrorNotification("", "", false); + WalletRegistration.toggleButtonsEnabled(false); + if (WalletRegistration.validateContactValue() === -1) return; + + function showNextPage(verificationField) { + const verificationFieldTitle = document.querySelector("label[for='verification']"); + const verificationFieldInput = document.querySelector("#verification"); + WalletRegistration.verificationField = verificationField; + + const inputFeldConfigMap = { + [VerificationField.DATE_OF_BIRTH]: { name: "date_of_birth", type: "date", label: "Date of birth" }, + [VerificationField.YEAR_MONTH]: { name: "year_month", type: "month", label: "Date of birth (Year/Month)" }, + [VerificationField.NATIONAL_ID_NUMBER]: { name: "national_id_number", type: "text", label: "National ID number" }, + [VerificationField.PIN]: { name: "pin", type: "text", label: "Pin" }, + }; + + const inputFieldConfig = inputFeldConfigMap[verificationField]; + if (inputFieldConfig) { + verificationFieldTitle.textContent = inputFieldConfig.label; + verificationFieldInput.name = inputFieldConfig.name; + verificationFieldInput.type = inputFieldConfig.type; } - sendSms(phoneNumber, reCAPTCHAToken, showNextPage, showErrorMessage); + WalletRegistration.setSection(CurrentSection.PASSCODE); + WalletRegistration.toggleButtonsEnabled(true); } + + function showErrorMessage(error) { + WalletRegistration.toggleErrorNotification("Error", error, true); + WalletRegistration.toggleButtonsEnabled(true); + } + + sendOtp(showNextPage, showErrorMessage); } +// ------------------------------ END: SECTION 2 ------------------------------ -document.addEventListener("DOMContentLoaded", function () { - const form = document.getElementById("submitOtpForm"); - form.addEventListener("submit", function (event) { - submitOtp(event); - }); -}); +// ------------------------------ START: SECTION 3 ------------------------------ +async function handleVerificationInfoSubmitted() { + const reCAPTCHAToken = WalletRegistration.getRecaptchaToken(); + if (!reCAPTCHAToken) { + WalletRegistration.toggleErrorNotification("Error", "reCAPTCHA is required", true); + return; + } -async function submitOtp(event) { - event.preventDefault(); - - const passcodeSectionEl = document.querySelector("[data-section='passcode']"); - const errorNotificationEl = document.querySelector( - "[data-section-error='passcode']" - ); - const successNotificationEl = document.querySelector( - "[data-section-success='passcode']" - ); - const otpEl = document.getElementById("otp"); - const verificationEl = document.getElementById("verification"); - const verificationField = verificationEl.getAttribute("name"); - - const buttonEls = passcodeSectionEl.querySelectorAll("[data-button]"); - - const reCAPTCHATokenEl = passcodeSectionEl.querySelector( - "#g-recaptcha-response-1" - ); - if (!reCAPTCHATokenEl || !reCAPTCHATokenEl.value) { - toggleErrorNotification( - errorNotificationEl, - "Error", - "reCAPTCHA is required", - true - ); + const contactMethod = WalletRegistration.contactMethod; + const contactValue = WalletRegistration.getContactValue(); + const otp = document.getElementById("otp").value; + const verificationFieldValue = document.getElementById("verification").value; + if (!contactMethod || !contactValue || !otp || !verificationFieldValue) { + const errMessage = `Missing one of the required fields: ${{ contactMethod, contactValue, otp, verificationFieldValue }}`; + WalletRegistration.toggleErrorNotification("Error", errMessage, true); return; } - if ( - WalletRegistration.intlTelInput && - otpEl && - verificationEl && - passcodeSectionEl && - errorNotificationEl - ) { - toggleErrorNotification(errorNotificationEl, "", "", false); - toggleSuccessNotification(successNotificationEl, "", "", false); - - const phoneNumber = WalletRegistration.intlTelInput.getNumber(); - const otp = otpEl.value; - const verification = verificationEl.value; - - if (phoneNumber && otp && verification) { - try { - disableButtons(buttonEls); - - const response = await fetch("/wallet-registration/verification", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${WalletRegistration.jwtToken}`, - }, - body: JSON.stringify({ - phone_number: phoneNumber, - otp: otp, - verification: verification, - verification_type: verificationField, - recaptcha_token: reCAPTCHATokenEl.value, - }), - }); - - if ([200, 201].includes(response.status)) { - await response.json(); - - const t = window.setTimeout(() => { - location.reload(); - clearTimeout(t); - }, 2000); - } else if (response.status === 400) { - const data = await response.json(); - const errorMessage = data.error || "Something went wrong, please try again later."; - throw new Error(errorMessage); - } else { - throw new Error("Something went wrong, please try again later."); - } - } catch (error) { - enableButtons(buttonEls); - toggleErrorNotification(errorNotificationEl, "Error", error, true); - grecaptcha.reset(1); - } + WalletRegistration.toggleErrorNotification("", "", false); + WalletRegistration.toggleSuccessNotification("", "", false); + + try { + WalletRegistration.toggleButtonsEnabled(false); + + const response = await fetch("/wallet-registration/verification", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${WalletRegistration.jwtToken}`, + }, + body: JSON.stringify({ + [contactMethod]: contactValue, + otp: otp, + recaptcha_token: reCAPTCHAToken, + verification_type: WalletRegistration.verificationField, + verification: verificationFieldValue, + }), + }); + + if (Math.floor(response.status / 100) === 2) { + await response.json(); + setTimeout(() => { + location.reload(); + }, 2000); + } else if (response.status === 400) { + const data = await response.json(); + const errorMessage = data.error || "Something went wrong with your request, please try again later."; + throw new Error(errorMessage); + } else { + throw new Error(`Something went wrong, please try again later (status code: ${response.status}).`); } + } catch (error) { + WalletRegistration.toggleButtonsEnabled(true); + WalletRegistration.toggleErrorNotification("Error", error, true); + grecaptcha.reset(1); } } -document.addEventListener("DOMContentLoaded", function () { - const button = document.getElementById("resendSmsButton"); - - button.addEventListener("click", function (event) { - resendSms(event); - }); -}); +async function handleResendOtpClicked() { + const reCAPTCHAToken = WalletRegistration.getRecaptchaToken(); + if (!reCAPTCHAToken) { + WalletRegistration.toggleErrorNotification("Error", "reCAPTCHA is required", true); + return; + } -async function resendSms() { - const passcodeSectionEl = document.querySelector("[data-section='passcode']"); - const errorNotificationEl = document.querySelector( - "[data-section-error='passcode']" - ); - const successNotificationEl = document.querySelector( - "[data-section-success='passcode']" - ); - const buttonEls = passcodeSectionEl.querySelectorAll("[data-button]"); - const reCAPTCHATokenEl = passcodeSectionEl.querySelector( - "#g-recaptcha-response-1" - ); - - if (!reCAPTCHATokenEl || !reCAPTCHATokenEl.value) { - toggleErrorNotification( - errorNotificationEl, - "Error", - "reCAPTCHA is required", - true - ); + const contactValue = WalletRegistration.getContactValue(); + if (!contactValue) { + WalletRegistration.toggleErrorNotification("Error", "Contact information is required", true); return; } - if ( - (passcodeSectionEl, - errorNotificationEl, - WalletRegistration.intlTelInput, - reCAPTCHATokenEl) - ) { - disableButtons(buttonEls); - toggleErrorNotification(errorNotificationEl, "", "", false); - toggleSuccessNotification(successNotificationEl, "", "", false); - - const phoneNumber = WalletRegistration.intlTelInput.getNumber(); - const reCAPTCHAToken = reCAPTCHATokenEl.value; - - function showErrorMessage(error) { - toggleErrorNotification(errorNotificationEl, "Error", error, true); - enableButtons(buttonEls); - } + WalletRegistration.toggleButtonsEnabled(false); + WalletRegistration.toggleErrorNotification("", "", false); + WalletRegistration.toggleSuccessNotification("", "", false); - function showSuccessMessage() { - toggleSuccessNotification( - successNotificationEl, - "New SMS sent", - "You will receive a new one-time passcode", - true - ); - enableButtons(buttonEls); - } + function showErrorMessage(error) { + WalletRegistration.toggleErrorNotification("Error", error, true); + WalletRegistration.toggleButtonsEnabled(true); + } - sendSms(phoneNumber, reCAPTCHAToken, showSuccessMessage, showErrorMessage); - grecaptcha.reset(1); + function showSuccessMessage() { + WalletRegistration.toggleSuccessNotification("New OTP sent", "You will receive a new one-time passcode", true); + WalletRegistration.toggleButtonsEnabled(true); } + + sendOtp(showSuccessMessage, showErrorMessage); + grecaptcha.reset(1); } +// ------------------------------ END: SECTION 3 ------------------------------ -function resetNumberInputError(buttonEls) { - if ( - WalletRegistration.phoneNumberErrorEl && - WalletRegistration.phoneNumberErrorEl.style.display !== "none" - ) { - toggleErrorNotification( - WalletRegistration.phoneNumberErrorEl, - "", - "", - false - ); - enableButtons(buttonEls); + +// ------------------------------ START: UTILITY FUNCTIONS ------------------------------ +function toggleNotification(type, { parentEl, title, message, isVisible }) { + const titleEl = parentEl.querySelector(`[data-section-${type}-title]`); + const messageEl = parentEl.querySelector(`[data-section-${type}-message`); + + if (titleEl && messageEl) { + parentEl.style.display = isVisible ? "flex" : "none"; + titleEl.innerHTML = isVisible ? title : ""; + messageEl.innerHTML = isVisible ? message : ""; } } -// Phone number input -// https://github.com/jackocnr/intl-tel-input -function phoneNumberInit() { - const phoneNumberInput = document.querySelector("#phone_number"); - const phoneNumberSectionEl = document.querySelector( - "[data-section='phoneNumber']" - ); - const buttonEls = phoneNumberSectionEl.querySelectorAll("[data-button]"); - - const intlTelInput = window.intlTelInput(phoneNumberInput, { - utilsScript: "/static/js/intl-tel-input-v18.2.1-utils.min.js", - separateDialCode: true, - preferredCountries: [], - // Excluding Cuba, Iran, North Korea, and Syria - excludeCountries: ["cu", "ir", "kp", "sy"], - // Setting default country based on user's IP address - initialCountry: "auto", - geoIpLookup: (callback) => { - fetch("https://ipapi.co/json") - .then((res) => res.json()) - .then((data) => callback(data.country_code)) - .catch(() => callback("")); - }, - }); +async function sendOtp(onSuccess, onError) { + const reqPayload = { + [WalletRegistration.contactMethod]: WalletRegistration.getContactValue(), + recaptcha_token: WalletRegistration.getRecaptchaToken(), + }; + + try { + const response = await fetch("/wallet-registration/otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${WalletRegistration.jwtToken}`, + }, + body: JSON.stringify(reqPayload), + }); - // Clear phone number error message - phoneNumberInput.addEventListener("change", () => - resetNumberInputError(buttonEls) - ); - phoneNumberInput.addEventListener("keyup", () => - resetNumberInputError(buttonEls) - ); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || "Something went wrong, please try again later."); + } - return intlTelInput; + onSuccess(data.verification_field); + } catch (error) { + onError(error); + } } - -// Init -window.onload = async () => { - WalletRegistration.jwtToken = getJwtToken(); - WalletRegistration.intlTelInput = phoneNumberInit(); - WalletRegistration.phoneNumberErrorEl = document.querySelector( - "[data-section-error='phoneNumber']" - ); - WalletRegistration.privacyPolicyLink = getPrivacyPolicyLink(); -}; +// ------------------------------ END: UTILITY FUNCTIONS ------------------------------ From 419f38a6f55e37416917e1a6643e089d7ef721a2 Mon Sep 17 00:00:00 2001 From: Marcelo Salloum dos Santos Date: Thu, 5 Sep 2024 15:29:34 -0700 Subject: [PATCH 21/75] [SDP-1331] Fix inconsistency in field names (#409) ### What Fix inconsistency in the usage of verification_field vs verification_type: 1. The JSON response from `/wallet-registration/otp` contains a field named `verification_field`, while `/wallet-registration/verification` was expecting that same field to be named `verification_type`. This was fixed in https://github.com/stellar/stellar-disbursement-platform-backend/commit/f06f8d62121311e1bd64988a1a11b6e56fc7b0bc. 2. Renamed the enum `VerificationField*` to `VerificationType*`. Fixed in https://github.com/stellar/stellar-disbursement-platform-backend/commit/2350f5f1742b8cbdad53cafd95ad7458461a0537. FYI: I tested the disbursement end2end with this change and it works as expected. Nothing was broken. ### Why The database has the following naming structure: - The enum for **type** is called `verification_type`, and it can contain values like DATE_OF_BIRTH, PIN, NATIONAL_ID and YEAR_AND_MONTH is called. - The column names that adhere to that type are called `verification_field`. That caused some confusion and led to some naming mistakes in the code. --- .../data/disbursement_instructions_test.go | 4 +- internal/data/disbursements.go | 21 +---- internal/data/disbursements_test.go | 12 +-- internal/data/fixtures.go | 4 +- internal/data/payments_test.go | 26 +++--- internal/data/receiver_verification.go | 16 ++-- internal/data/receiver_verification_test.go | 80 +++++++++---------- internal/data/receivers.go | 12 +-- internal/data/receivers_test.go | 4 +- internal/data/verification_type.go | 20 +++++ .../integrationtests/integration_tests.go | 4 +- internal/integrationtests/server_api_test.go | 2 +- ...h_anchor_platform_transactions_job_test.go | 2 +- .../serve/httphandler/disbursement_handler.go | 14 ++-- .../httphandler/disbursement_handler_test.go | 12 +-- .../httphandler/payments_handler_test.go | 2 +- .../serve/httphandler/receiver_handler.go | 2 +- .../httphandler/receiver_send_otp_handler.go | 7 +- .../receiver_send_otp_handler_test.go | 2 +- .../httphandler/update_receiver_handler.go | 12 +-- .../update_receiver_handler_test.go | 52 ++++++------ .../verifiy_receiver_registration_handler.go | 10 +-- ...ifiy_receiver_registration_handler_test.go | 46 +++++------ .../publicfiles/js/receiver_registration.js | 2 +- .../disbursement_instructions_validator.go | 12 +-- ...isbursement_instructions_validator_test.go | 36 ++++----- .../disbursement_request_validator.go | 10 +-- .../disbursement_request_validator_test.go | 12 +-- .../receiver_registration_validator.go | 26 +++--- .../receiver_registration_validator_test.go | 48 +++++------ ...r_platform_transactions_completion_test.go | 28 +++---- ...ready_payments_cancelation_service_test.go | 2 +- 32 files changed, 271 insertions(+), 271 deletions(-) create mode 100644 internal/data/verification_type.go diff --git a/internal/data/disbursement_instructions_test.go b/internal/data/disbursement_instructions_test.go index 84082614e..4d7c3398d 100644 --- a/internal/data/disbursement_instructions_test.go +++ b/internal/data/disbursement_instructions_test.go @@ -79,7 +79,7 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { assertEqualReceivers(t, expectedPhoneNumbers, expectedExternalIDs, receivers) // Verify ReceiverVerifications - receiverVerifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID, receivers[2].ID}, VerificationFieldDateOfBirth) + receiverVerifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID, receivers[2].ID}, VerificationTypeDateOfBirth) require.NoError(t, err) assertEqualVerifications(t, instructions, receiverVerifications, receivers) @@ -128,7 +128,7 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { assertEqualReceivers(t, expectedPhoneNumbers, expectedExternalIDs, receivers) // Verify ReceiverVerifications - receiverVerifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID, receivers[2].ID}, VerificationFieldDateOfBirth) + receiverVerifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID, receivers[2].ID}, VerificationTypeDateOfBirth) require.NoError(t, err) assertEqualVerifications(t, instructions, receiverVerifications, receivers) diff --git a/internal/data/disbursements.go b/internal/data/disbursements.go index 8010d27f4..59b85dbbc 100644 --- a/internal/data/disbursements.go +++ b/internal/data/disbursements.go @@ -23,7 +23,7 @@ type Disbursement struct { Wallet *Wallet `json:"wallet,omitempty" db:"wallet"` Asset *Asset `json:"asset,omitempty" db:"asset"` Status DisbursementStatus `json:"status" db:"status"` - VerificationField VerificationField `json:"verification_field,omitempty" db:"verification_field"` + VerificationField VerificationType `json:"verification_field,omitempty" db:"verification_field"` StatusHistory DisbursementStatusHistory `json:"status_history,omitempty" db:"status_history"` SMSRegistrationMessageTemplate string `json:"sms_registration_message_template" db:"sms_registration_message_template"` FileName string `json:"file_name,omitempty" db:"file_name"` @@ -52,25 +52,6 @@ type DisbursementUpdate struct { FileContent []byte } -type VerificationField string - -const ( - VerificationFieldDateOfBirth VerificationField = "DATE_OF_BIRTH" - VerificationFieldYearMonth VerificationField = "YEAR_MONTH" - VerificationFieldPin VerificationField = "PIN" - VerificationFieldNationalID VerificationField = "NATIONAL_ID_NUMBER" -) - -// GetAllVerificationFields returns all verification fields -func GetAllVerificationFields() []VerificationField { - return []VerificationField{ - VerificationFieldDateOfBirth, - VerificationFieldYearMonth, - VerificationFieldPin, - VerificationFieldNationalID, - } -} - type DisbursementStatusHistoryEntry struct { UserID string `json:"user_id"` Status DisbursementStatus `json:"status"` diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go index e764d12a8..cd5cf313d 100644 --- a/internal/data/disbursements_test.go +++ b/internal/data/disbursements_test.go @@ -43,7 +43,7 @@ func Test_DisbursementModelInsert(t *testing.T) { Asset: asset, Country: country, Wallet: wallet, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, SMSRegistrationMessageTemplate: smsTemplate, } @@ -73,7 +73,7 @@ func Test_DisbursementModelInsert(t *testing.T) { assert.Equal(t, 1, len(actual.StatusHistory)) assert.Equal(t, DraftDisbursementStatus, actual.StatusHistory[0].Status) assert.Equal(t, "user1", actual.StatusHistory[0].UserID) - assert.Equal(t, VerificationFieldDateOfBirth, actual.VerificationField) + assert.Equal(t, VerificationTypeDateOfBirth, actual.VerificationField) }) } @@ -539,7 +539,7 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) { Asset: asset, Wallet: wallet, Country: country, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) _ = CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -567,7 +567,7 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) { Asset: asset, Wallet: wallet, Country: country, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) _ = CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -605,7 +605,7 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) { Asset: asset, Wallet: wallet, Country: country, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) _ = CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -624,7 +624,7 @@ func Test_DisbursementModel_CompleteDisbursements(t *testing.T) { Asset: asset, Wallet: wallet, Country: country, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) _ = CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go index c0353bf80..5df2bc928 100644 --- a/internal/data/fixtures.go +++ b/internal/data/fixtures.go @@ -541,7 +541,7 @@ func CreateDisbursementFixture(t *testing.T, ctx context.Context, sqlExec db.SQL d.Country = GetCountryFixture(t, ctx, sqlExec, FixtureCountryUKR) } if d.VerificationField == "" { - d.VerificationField = VerificationFieldDateOfBirth + d.VerificationField = VerificationTypeDateOfBirth } // insert disbursement @@ -614,7 +614,7 @@ func CreateDraftDisbursementFixture(t *testing.T, ctx context.Context, sqlExec d } if insert.VerificationField == "" { - insert.VerificationField = VerificationFieldDateOfBirth + insert.VerificationField = VerificationTypeDateOfBirth } id, err := model.Insert(ctx, &insert) diff --git a/internal/data/payments_test.go b/internal/data/payments_test.go index dc4200aba..0efe50ef1 100644 --- a/internal/data/payments_test.go +++ b/internal/data/payments_test.go @@ -721,7 +721,7 @@ func Test_PaymentModelRetryFailedPayments(t *testing.T) { Wallet: wallet, Asset: asset, Status: ReadyDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) t.Run("does not update payments when no payments IDs is given", func(t *testing.T) { @@ -992,7 +992,7 @@ func Test_PaymentModelCancelPayment(t *testing.T) { Wallet: wallet, Asset: asset, Status: ReadyDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) t.Run("no ready payment for more than 5 days won't cancel any", func(t *testing.T) { @@ -1251,7 +1251,7 @@ func Test_PaymentModel_GetReadyByDisbursementID(t *testing.T) { Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) t.Run("returns empty array when there's no payment ready", func(t *testing.T) { @@ -1317,7 +1317,7 @@ func Test_PaymentModel_GetReadyByPaymentsID(t *testing.T) { Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) t.Run("returns empty array when there's no payment ready", func(t *testing.T) { @@ -1391,7 +1391,7 @@ func Test_PaymentModel_GetReadyByReceiverWalletID(t *testing.T) { Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) t.Run("returns empty array when there's no payment ready", func(t *testing.T) { @@ -1475,7 +1475,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) // It's not possible to have a payment in a end state when the receiver wallet is not registered yet @@ -1510,7 +1510,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) _ = CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -1545,7 +1545,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) paymentReceiver1 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -1596,7 +1596,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{ @@ -1604,7 +1604,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) payment1 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -1657,7 +1657,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet1, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) disbursement2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{ @@ -1665,7 +1665,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet2, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) payment1 := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -1730,7 +1730,7 @@ func Test_PaymentModel_GetAllReadyToPatchCompletionAnchorTransactions(t *testing Wallet: wallet, Asset: asset, Status: StartedDisbursementStatus, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }) payment := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ diff --git a/internal/data/receiver_verification.go b/internal/data/receiver_verification.go index 4a2b469e8..1de538a79 100644 --- a/internal/data/receiver_verification.go +++ b/internal/data/receiver_verification.go @@ -18,7 +18,7 @@ import ( type ReceiverVerification struct { ReceiverID string `json:"receiver_id" db:"receiver_id"` - VerificationField VerificationField `json:"verification_field" db:"verification_field"` + VerificationField VerificationType `json:"verification_field" db:"verification_field"` HashedValue string `json:"hashed_value" db:"hashed_value"` Attempts int `json:"attempts" db:"attempts"` CreatedAt time.Time `json:"created_at" db:"created_at"` @@ -33,9 +33,9 @@ type ReceiverVerificationModel struct { } type ReceiverVerificationInsert struct { - ReceiverID string `db:"receiver_id"` - VerificationField VerificationField `db:"verification_field"` - VerificationValue string `db:"hashed_value"` + ReceiverID string `db:"receiver_id"` + VerificationField VerificationType `db:"verification_field"` + VerificationValue string `db:"hashed_value"` } const MaxAttemptsAllowed = 15 @@ -54,7 +54,7 @@ func (rvi *ReceiverVerificationInsert) Validate() error { } // GetByReceiverIDsAndVerificationField returns receiver verifications by receiver IDs and verification type. -func (m *ReceiverVerificationModel) GetByReceiverIDsAndVerificationField(ctx context.Context, sqlExec db.SQLExecuter, receiverIds []string, verificationField VerificationField) ([]*ReceiverVerification, error) { +func (m *ReceiverVerificationModel) GetByReceiverIDsAndVerificationField(ctx context.Context, sqlExec db.SQLExecuter, receiverIds []string, verificationField VerificationType) ([]*ReceiverVerification, error) { receiverVerifications := []*ReceiverVerification{} query := ` SELECT @@ -156,7 +156,7 @@ func (m *ReceiverVerificationModel) Insert(ctx context.Context, sqlExec db.SQLEx func (m *ReceiverVerificationModel) UpdateVerificationValue(ctx context.Context, sqlExec db.SQLExecuter, receiverID string, - verificationField VerificationField, + verificationField VerificationType, verificationValue string, ) error { log.Ctx(ctx).Infof("Calling UpdateVerificationValue for receiver %s and verification field %s", receiverID, verificationField) @@ -181,7 +181,7 @@ func (m *ReceiverVerificationModel) UpdateVerificationValue(ctx context.Context, // UpsertVerificationValue creates or updates the receiver's verification. In case the verification exists and it's already confirmed by the receiver // it's not updated. -func (m *ReceiverVerificationModel) UpsertVerificationValue(ctx context.Context, sqlExec db.SQLExecuter, receiverID string, verificationField VerificationField, verificationValue string) error { +func (m *ReceiverVerificationModel) UpsertVerificationValue(ctx context.Context, sqlExec db.SQLExecuter, receiverID string, verificationField VerificationType, verificationValue string) error { log.Ctx(ctx).Infof("Calling UpsertVerificationValue for receiver %s and verification field %s", receiverID, verificationField) hashedValue, err := HashVerificationValue(verificationValue) if err != nil { @@ -211,7 +211,7 @@ func (m *ReceiverVerificationModel) UpsertVerificationValue(ctx context.Context, type ReceiverVerificationUpdate struct { ReceiverID string `db:"receiver_id"` - VerificationField VerificationField `db:"verification_field"` + VerificationField VerificationType `db:"verification_field"` VerificationChannel message.MessageChannel `db:"verification_channel"` Attempts *int `db:"attempts"` ConfirmedAt *time.Time `db:"confirmed_at"` diff --git a/internal/data/receiver_verification_test.go b/internal/data/receiver_verification_test.go index 8adf97d27..505dac147 100644 --- a/internal/data/receiver_verification_test.go +++ b/internal/data/receiver_verification_test.go @@ -31,17 +31,17 @@ func Test_ReceiverVerificationModel_GetByReceiverIDsAndVerificationField(t *test verification1 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver1.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) verification2 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver2.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-02", }) CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver3.ID, - VerificationField: VerificationFieldPin, + VerificationField: VerificationTypePin, VerificationValue: "1990-01-03", }) @@ -50,11 +50,11 @@ func Test_ReceiverVerificationModel_GetByReceiverIDsAndVerificationField(t *test receiverVerificationModel := ReceiverVerificationModel{} - actualVerifications, err := receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receiver1.ID, receiver2.ID, receiver3.ID}, VerificationFieldDateOfBirth) + actualVerifications, err := receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receiver1.ID, receiver2.ID, receiver3.ID}, VerificationTypeDateOfBirth) require.NoError(t, err) assert.Equal(t, 2, len(actualVerifications)) for _, v := range actualVerifications { - assert.Equal(t, VerificationFieldDateOfBirth, v.VerificationField) + assert.Equal(t, VerificationTypeDateOfBirth, v.VerificationField) assert.Contains(t, verifiedReceivers, v.ReceiverID) assert.Contains(t, verifieldValues, v.HashedValue) } @@ -84,22 +84,22 @@ func Test_ReceiverVerificationModel_GetAllByReceiverId(t *testing.T) { t.Run("returns all when the receiver has verifications registered", func(t *testing.T) { verification1 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) verification2 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldYearMonth, + VerificationField: VerificationTypeYearMonth, VerificationValue: "1990-01", }) verification3 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldPin, + VerificationField: VerificationTypePin, VerificationValue: "1234", }) verification4 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldNationalID, + VerificationField: VerificationTypeNationalID, VerificationValue: "5678", }) @@ -111,28 +111,28 @@ func Test_ReceiverVerificationModel_GetAllByReceiverId(t *testing.T) { assert.Equal(t, []ReceiverVerification{ { ReceiverID: receiver.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, HashedValue: verification1.HashedValue, CreatedAt: verification1.CreatedAt, UpdatedAt: verification1.UpdatedAt, }, { ReceiverID: receiver.ID, - VerificationField: VerificationFieldYearMonth, + VerificationField: VerificationTypeYearMonth, HashedValue: verification2.HashedValue, CreatedAt: verification2.CreatedAt, UpdatedAt: verification2.UpdatedAt, }, { ReceiverID: receiver.ID, - VerificationField: VerificationFieldPin, + VerificationField: VerificationTypePin, HashedValue: verification3.HashedValue, CreatedAt: verification3.CreatedAt, UpdatedAt: verification3.UpdatedAt, }, { ReceiverID: receiver.ID, - VerificationField: VerificationFieldNationalID, + VerificationField: VerificationTypeNationalID, HashedValue: verification4.HashedValue, CreatedAt: verification4.CreatedAt, UpdatedAt: verification4.UpdatedAt, @@ -165,28 +165,28 @@ func Test_ReceiverVerificationModel_GetReceiverVerificationByReceiverId(t *testi earlierTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) verification1 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) verification1.UpdatedAt = earlierTime verification2 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldYearMonth, + VerificationField: VerificationTypeYearMonth, VerificationValue: "1990-01", }) verification2.UpdatedAt = earlierTime verification3 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldPin, + VerificationField: VerificationTypePin, VerificationValue: "1234", }) verification3.UpdatedAt = earlierTime verification4 := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldNationalID, + VerificationField: VerificationTypeNationalID, VerificationValue: "5678", }) @@ -197,7 +197,7 @@ func Test_ReceiverVerificationModel_GetReceiverVerificationByReceiverId(t *testi assert.Equal(t, ReceiverVerification{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldNationalID, + VerificationField: VerificationTypeNationalID, HashedValue: verification4.HashedValue, CreatedAt: verification4.CreatedAt, UpdatedAt: verification4.UpdatedAt, @@ -221,14 +221,14 @@ func Test_ReceiverVerificationModel_Insert(t *testing.T) { verification := ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", } _, err = receiverVerificationModel.Insert(ctx, dbConnectionPool, verification) require.NoError(t, err) - actualVerification, err := receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receiver.ID}, VerificationFieldDateOfBirth) + actualVerification, err := receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receiver.ID}, VerificationTypeDateOfBirth) require.NoError(t, err) verified := CompareVerificationValue(actualVerification[0].HashedValue, verification.VerificationValue) assert.True(t, verified) @@ -253,7 +253,7 @@ func Test_ReceiverVerificationModel_UpdateVerificationValue(t *testing.T) { oldExpectedValue := "1990-01-01" actualBeforeUpdate, err := receiverVerificationModel.Insert(ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: oldExpectedValue, }) require.NoError(t, err) @@ -262,10 +262,10 @@ func Test_ReceiverVerificationModel_UpdateVerificationValue(t *testing.T) { assert.True(t, verified) newExpectedValue := "1990-01-02" - err = receiverVerificationModel.UpdateVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationFieldDateOfBirth, newExpectedValue) + err = receiverVerificationModel.UpdateVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypeDateOfBirth, newExpectedValue) require.NoError(t, err) - actualAfterUpdate, err := receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receiver.ID}, VerificationFieldDateOfBirth) + actualAfterUpdate, err := receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receiver.ID}, VerificationTypeDateOfBirth) require.NoError(t, err) verified = CompareVerificationValue(actualAfterUpdate[0].HashedValue, newExpectedValue) assert.True(t, verified) @@ -282,7 +282,7 @@ func Test_ReceiverVerificationModel_UpsertVerificationValue(t *testing.T) { ctx := context.Background() receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{}) receiverVerificationModel := ReceiverVerificationModel{} - getReceiverVerificationHashedValue := func(t *testing.T, ctx context.Context, dbConnectionPool db.DBConnectionPool, receiverID string, verificationField VerificationField) string { + getReceiverVerificationHashedValue := func(t *testing.T, ctx context.Context, dbConnectionPool db.DBConnectionPool, receiverID string, verificationField VerificationType) string { const q = "SELECT hashed_value FROM receiver_verifications WHERE receiver_id = $1 AND verification_field = $2" var hashedValue string qErr := dbConnectionPool.GetContext(ctx, &hashedValue, q, receiverID, verificationField) @@ -293,20 +293,20 @@ func Test_ReceiverVerificationModel_UpsertVerificationValue(t *testing.T) { t.Run("upserts the verification value successfully", func(t *testing.T) { // Inserts the verification value firstVerificationValue := "123456" - err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationFieldPin, firstVerificationValue) + err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypePin, firstVerificationValue) require.NoError(t, err) - currentHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationFieldPin) + currentHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationTypePin) assert.NotEmpty(t, currentHashedValue) verified := CompareVerificationValue(currentHashedValue, firstVerificationValue) assert.True(t, verified) // Updates the verification value newVerificationValue := "654321" - err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationFieldPin, newVerificationValue) + err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypePin, newVerificationValue) require.NoError(t, err) - afterUpdateHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationFieldPin) + afterUpdateHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationTypePin) assert.NotEmpty(t, afterUpdateHashedValue) // Checking if the hashed value is NOT the first one. @@ -320,10 +320,10 @@ func Test_ReceiverVerificationModel_UpsertVerificationValue(t *testing.T) { t.Run("doesn't update the verification value when it was confirmed by the receiver", func(t *testing.T) { // Inserts the verification value firstVerificationValue := "0301016957187" - err := receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationFieldNationalID, firstVerificationValue) + err := receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypeNationalID, firstVerificationValue) require.NoError(t, err) - currentHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationFieldNationalID) + currentHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationTypeNationalID) assert.NotEmpty(t, currentHashedValue) verified := CompareVerificationValue(currentHashedValue, firstVerificationValue) assert.True(t, verified) @@ -332,17 +332,17 @@ func Test_ReceiverVerificationModel_UpsertVerificationValue(t *testing.T) { now := time.Now() err = receiverVerificationModel.UpdateReceiverVerification(ctx, ReceiverVerificationUpdate{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldNationalID, + VerificationField: VerificationTypeNationalID, ConfirmedAt: &now, VerificationChannel: message.MessageChannelSMS, }, dbConnectionPool) require.NoError(t, err) newVerificationValue := "0301017821085" - err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationFieldNationalID, newVerificationValue) + err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypeNationalID, newVerificationValue) require.NoError(t, err) - afterUpdateHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationFieldNationalID) + afterUpdateHashedValue := getReceiverVerificationHashedValue(t, ctx, dbConnectionPool, receiver.ID, VerificationTypeNationalID) assert.NotEmpty(t, currentHashedValue) // Checking if the hashed value is NOT the new one. @@ -371,7 +371,7 @@ func Test_ReceiverVerificationModel_UpdateReceiverVerification(t *testing.T) { verification := CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) @@ -382,7 +382,7 @@ func Test_ReceiverVerificationModel_UpdateReceiverVerification(t *testing.T) { date := time.Date(2023, 1, 10, 23, 40, 20, 1000, time.UTC) verificationUpdate := ReceiverVerificationUpdate{ ReceiverID: receiver.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, Attempts: utils.IntPtr(5), ConfirmedAt: &date, FailedAt: &date, @@ -424,7 +424,7 @@ func Test_ReceiverVerificationUpdate_Validate(t *testing.T) { name: "valid update", update: ReceiverVerificationUpdate{ ReceiverID: "receiver-id", - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationChannel: message.MessageChannelSMS, }, wantErr: nil, @@ -445,7 +445,7 @@ func Test_ReceiverVerificationUpdate_Validate(t *testing.T) { name: "invalid update with empty verification channel", update: ReceiverVerificationUpdate{ ReceiverID: "receiver-id", - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, }, wantErr: fmt.Errorf("verification channel is required"), }, @@ -492,15 +492,15 @@ func Test_ReceiverVerificationModel_GetLatestByPhoneNumber(t *testing.T) { receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{}) receiverVerificationModel := ReceiverVerificationModel{dbConnectionPool: dbConnectionPool} - err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationFieldDateOfBirth, "1990-01-01") + err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypeDateOfBirth, "1990-01-01") require.NoError(t, err) - err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationFieldPin, "123456") + err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypePin, "123456") require.NoError(t, err) verification, err := receiverVerificationModel.GetLatestByPhoneNumber(ctx, receiver.PhoneNumber) require.NoError(t, err) - assert.Equal(t, VerificationFieldPin, verification.VerificationField) + assert.Equal(t, VerificationTypePin, verification.VerificationField) assert.True(t, CompareVerificationValue(verification.HashedValue, "123456")) } diff --git a/internal/data/receivers.go b/internal/data/receivers.go index c21be43fd..16fc3545f 100644 --- a/internal/data/receivers.go +++ b/internal/data/receivers.go @@ -27,12 +27,12 @@ type Receiver struct { type ReceiverRegistrationRequest struct { // TODO: SDP-1296 - Update `/wallet-registration/otp` to support multiple contact information types and send OTPs accordingly - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - OTP string `json:"otp"` - VerificationValue string `json:"verification"` - VerificationType VerificationField `json:"verification_type"` - ReCAPTCHAToken string `json:"recaptcha_token"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + OTP string `json:"otp"` + VerificationValue string `json:"verification"` + VerificationField VerificationType `json:"verification_field"` + ReCAPTCHAToken string `json:"recaptcha_token"` } type ReceiverStats struct { diff --git a/internal/data/receivers_test.go b/internal/data/receivers_test.go index 9467807c2..8878356d3 100644 --- a/internal/data/receivers_test.go +++ b/internal/data/receivers_test.go @@ -929,7 +929,7 @@ func Test_DeleteByPhoneNumber(t *testing.T) { receiverWalletX := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverX.ID, wallet.ID, DraftReceiversWalletStatus) _ = CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiverX.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) messageX := CreateMessageFixture(t, ctx, dbConnectionPool, &Message{ @@ -960,7 +960,7 @@ func Test_DeleteByPhoneNumber(t *testing.T) { receiverWalletY := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverY.ID, wallet.ID, DraftReceiversWalletStatus) _ = CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, ReceiverVerificationInsert{ ReceiverID: receiverY.ID, - VerificationField: VerificationFieldDateOfBirth, + VerificationField: VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) messageY := CreateMessageFixture(t, ctx, dbConnectionPool, &Message{ diff --git a/internal/data/verification_type.go b/internal/data/verification_type.go new file mode 100644 index 000000000..a5d725d59 --- /dev/null +++ b/internal/data/verification_type.go @@ -0,0 +1,20 @@ +package data + +type VerificationType string + +const ( + VerificationTypeDateOfBirth VerificationType = "DATE_OF_BIRTH" + VerificationTypeYearMonth VerificationType = "YEAR_MONTH" + VerificationTypePin VerificationType = "PIN" + VerificationTypeNationalID VerificationType = "NATIONAL_ID_NUMBER" +) + +// GetAllVerificationTypes returns all the available verification types. +func GetAllVerificationTypes() []VerificationType { + return []VerificationType{ + VerificationTypeDateOfBirth, + VerificationTypeYearMonth, + VerificationTypePin, + VerificationTypeNationalID, + } +} diff --git a/internal/integrationtests/integration_tests.go b/internal/integrationtests/integration_tests.go index db209500c..6cbc60adb 100644 --- a/internal/integrationtests/integration_tests.go +++ b/internal/integrationtests/integration_tests.go @@ -180,7 +180,7 @@ func (it *IntegrationTestsService) StartIntegrationTests(ctx context.Context, op CountryCode: "USA", WalletID: wallet.ID, AssetID: asset.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) if err != nil { return fmt.Errorf("creating disbursement: %w", err) @@ -254,7 +254,7 @@ func (it *IntegrationTestsService) StartIntegrationTests(ctx context.Context, op OTP: data.TestnetAlwaysValidOTP, PhoneNumber: disbursementData[0].Phone, VerificationValue: disbursementData[0].VerificationValue, - VerificationType: disbursement.VerificationField, + VerificationField: disbursement.VerificationField, ReCAPTCHAToken: opts.RecaptchaSiteKey, }) if err != nil { diff --git a/internal/integrationtests/server_api_test.go b/internal/integrationtests/server_api_test.go index 75815ac82..a3f84e5c4 100644 --- a/internal/integrationtests/server_api_test.go +++ b/internal/integrationtests/server_api_test.go @@ -307,7 +307,7 @@ func Test_ReceiverRegistration(t *testing.T) { PhoneNumber: "+18554212274", OTP: "123456", VerificationValue: "1999-01-30", - VerificationType: "date_of_birth", + VerificationField: "date_of_birth", ReCAPTCHAToken: "captchtoken", } 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 2474c1abb..b0366454a 100644 --- a/internal/scheduler/jobs/patch_anchor_platform_transactions_job_test.go +++ b/internal/scheduler/jobs/patch_anchor_platform_transactions_job_test.go @@ -149,7 +149,7 @@ func Test_PatchAnchorPlatformTransactionsCompletionJob_Execute(t *testing.T) { Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go index 52b187ddc..250879d5f 100644 --- a/internal/serve/httphandler/disbursement_handler.go +++ b/internal/serve/httphandler/disbursement_handler.go @@ -37,12 +37,12 @@ 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.VerificationField `json:"verification_field"` - SMSRegistrationMessageTemplate string `json:"sms_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"` + SMSRegistrationMessageTemplate string `json:"sms_registration_message_template"` } type PatchDisbursementStatusRequest struct { @@ -438,7 +438,7 @@ func (d DisbursementHandler) GetDisbursementInstructions(w http.ResponseWriter, } } -func parseInstructionsFromCSV(ctx context.Context, file io.Reader, verificationField data.VerificationField) ([]*data.DisbursementInstruction, *validators.DisbursementInstructionsValidator) { +func parseInstructionsFromCSV(ctx context.Context, file io.Reader, verificationField data.VerificationType) ([]*data.DisbursementInstruction, *validators.DisbursementInstructionsValidator) { validator := validators.NewDisbursementInstructionsValidator(verificationField) instructions := []*data.DisbursementInstruction{} diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go index b414273ce..7c14151a5 100644 --- a/internal/serve/httphandler/disbursement_handler_test.go +++ b/internal/serve/httphandler/disbursement_handler_test.go @@ -175,7 +175,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { CountryCode: country.Code, AssetID: asset.ID, WalletID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b", - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) require.NoError(t, err) @@ -191,7 +191,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { CountryCode: country.Code, AssetID: asset.ID, WalletID: disabledWallet.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) require.NoError(t, err) @@ -206,7 +206,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { CountryCode: country.Code, AssetID: "aab4a4a9-2493-4f37-9741-01d5bd31d68b", WalletID: enabledWallet.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) require.NoError(t, err) @@ -221,7 +221,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { CountryCode: "AAA", AssetID: asset.ID, WalletID: enabledWallet.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) require.NoError(t, err) @@ -244,7 +244,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { CountryCode: country.Code, AssetID: asset.ID, WalletID: enabledWallet.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) require.NoError(t, err) @@ -265,7 +265,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { CountryCode: country.Code, AssetID: asset.ID, WalletID: enabledWallet.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, SMSRegistrationMessageTemplate: smsTemplate, }) require.NoError(t, err) diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go index cd5abcce3..869ab904b 100644 --- a/internal/serve/httphandler/payments_handler_test.go +++ b/internal/serve/httphandler/payments_handler_test.go @@ -803,7 +803,7 @@ func Test_PaymentHandler_RetryPayments(t *testing.T) { Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) t.Run("returns Unauthorized when no token in the context", func(t *testing.T) { diff --git a/internal/serve/httphandler/receiver_handler.go b/internal/serve/httphandler/receiver_handler.go index ccc30eac9..847c5d613 100644 --- a/internal/serve/httphandler/receiver_handler.go +++ b/internal/serve/httphandler/receiver_handler.go @@ -137,5 +137,5 @@ func (rh ReceiverHandler) GetReceivers(w http.ResponseWriter, r *http.Request) { // GetReceiverVerification returns a list of verification types func (rh ReceiverHandler) GetReceiverVerificationTypes(w http.ResponseWriter, r *http.Request) { - httpjson.Render(w, data.GetAllVerificationFields(), httpjson.JSON) + httpjson.Render(w, data.GetAllVerificationTypes(), httpjson.JSON) } diff --git a/internal/serve/httphandler/receiver_send_otp_handler.go b/internal/serve/httphandler/receiver_send_otp_handler.go index 00e9a38c7..3b321a1b5 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler.go +++ b/internal/serve/httphandler/receiver_send_otp_handler.go @@ -40,11 +40,10 @@ type ReceiverSendOTPRequest struct { } type ReceiverSendOTPResponseBody struct { - Message string `json:"message"` - VerificationField data.VerificationField `json:"verification_field"` + Message string `json:"message"` + VerificationField data.VerificationType `json:"verification_field"` } -// FIXME! /wallet-registration/otp returns a JSON with the field named `verification_field` but /wallet-registration/verification expects the field to be named `verification_type`. This inconsistency should be fixed. func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -98,7 +97,7 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } - verificationField := data.VerificationFieldDateOfBirth + verificationField := data.VerificationTypeDateOfBirth receiverVerification, err := h.Models.ReceiverVerification.GetLatestByPhoneNumber(ctx, receiverSendOTPRequest.PhoneNumber) if err != nil { err = fmt.Errorf("cannot find latest receiver verification for phone number %s: %w", truncatedPhoneNumber, err) diff --git a/internal/serve/httphandler/receiver_send_otp_handler_test.go b/internal/serve/httphandler/receiver_send_otp_handler_test.go index 1d3e7baf8..308747a8a 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler_test.go +++ b/internal/serve/httphandler/receiver_send_otp_handler_test.go @@ -47,7 +47,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { wallet1 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://home.page", "home.page", "wallet123://") data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver1.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.RegisteredReceiversWalletStatus) diff --git a/internal/serve/httphandler/update_receiver_handler.go b/internal/serve/httphandler/update_receiver_handler.go index d9eb52c58..0d84752d4 100644 --- a/internal/serve/httphandler/update_receiver_handler.go +++ b/internal/serve/httphandler/update_receiver_handler.go @@ -23,7 +23,7 @@ type UpdateReceiverHandler struct { func createVerificationInsert(updateReceiverInfo *validators.UpdateReceiverRequest, receiverID string) []data.ReceiverVerificationInsert { receiverVerifications := []data.ReceiverVerificationInsert{} - appendNewVerificationValue := func(verificationField data.VerificationField, verificationValue string) { + appendNewVerificationValue := func(verificationField data.VerificationType, verificationValue string) { if verificationValue != "" { receiverVerifications = append(receiverVerifications, data.ReceiverVerificationInsert{ ReceiverID: receiverID, @@ -33,15 +33,15 @@ func createVerificationInsert(updateReceiverInfo *validators.UpdateReceiverReque } } - for _, verificationField := range data.GetAllVerificationFields() { + for _, verificationField := range data.GetAllVerificationTypes() { switch verificationField { - case data.VerificationFieldDateOfBirth: + case data.VerificationTypeDateOfBirth: appendNewVerificationValue(verificationField, updateReceiverInfo.DateOfBirth) - case data.VerificationFieldYearMonth: + case data.VerificationTypeYearMonth: appendNewVerificationValue(verificationField, updateReceiverInfo.YearMonth) - case data.VerificationFieldPin: + case data.VerificationTypePin: appendNewVerificationValue(verificationField, updateReceiverInfo.Pin) - case data.VerificationFieldNationalID: + case data.VerificationTypeNationalID: appendNewVerificationValue(verificationField, updateReceiverInfo.NationalID) } } diff --git a/internal/serve/httphandler/update_receiver_handler_test.go b/internal/serve/httphandler/update_receiver_handler_test.go index 5a267d1a6..47b069403 100644 --- a/internal/serve/httphandler/update_receiver_handler_test.go +++ b/internal/serve/httphandler/update_receiver_handler_test.go @@ -26,25 +26,25 @@ func Test_UpdateReceiverHandler_createVerificationInsert(t *testing.T) { verificationDOB := data.ReceiverVerificationInsert{ ReceiverID: receiverID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1999-01-01", } verificationYearMonth := data.ReceiverVerificationInsert{ ReceiverID: receiverID, - VerificationField: data.VerificationFieldYearMonth, + VerificationField: data.VerificationTypeYearMonth, VerificationValue: "1999-01", } verificationPIN := data.ReceiverVerificationInsert{ ReceiverID: receiverID, - VerificationField: data.VerificationFieldPin, + VerificationField: data.VerificationTypePin, VerificationValue: "123", } verificationNationalID := data.ReceiverVerificationInsert{ ReceiverID: receiverID, - VerificationField: data.VerificationFieldNationalID, + VerificationField: data.VerificationTypeNationalID, VerificationValue: "12345CODE", } @@ -268,7 +268,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { t.Run("update date of birth value", func(t *testing.T) { data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "2000-01-01", }) @@ -297,7 +297,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { ` newReceiverVerification := data.ReceiverVerification{} - err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationFieldDateOfBirth) + err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypeDateOfBirth) require.NoError(t, err) assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "1999-01-01")) @@ -312,7 +312,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { t.Run("update year/month value", func(t *testing.T) { data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldYearMonth, + VerificationField: data.VerificationTypeYearMonth, VerificationValue: "2000-01", }) @@ -341,7 +341,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { ` newReceiverVerification := data.ReceiverVerification{} - err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationFieldYearMonth) + err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypeYearMonth) require.NoError(t, err) assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "1999-01")) @@ -356,7 +356,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { t.Run("update pin value", func(t *testing.T) { data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldPin, + VerificationField: data.VerificationTypePin, VerificationValue: "8901", }) @@ -385,7 +385,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { ` newReceiverVerification := data.ReceiverVerification{} - err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationFieldPin) + err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypePin) require.NoError(t, err) assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "1234")) @@ -400,7 +400,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { t.Run("update national ID value", func(t *testing.T) { data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldNationalID, + VerificationField: data.VerificationTypeNationalID, VerificationValue: "OLDID890", }) @@ -429,7 +429,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { ` newReceiverVerification := data.ReceiverVerification{} - err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationFieldNationalID) + err = dbConnectionPool.GetContext(ctx, &newReceiverVerification, query, receiver.ID, data.VerificationTypeNationalID) require.NoError(t, err) assert.True(t, data.CompareVerificationValue(newReceiverVerification.HashedValue, "NEWID123")) @@ -446,25 +446,25 @@ func Test_UpdateReceiverHandler(t *testing.T) { data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "2000-01-01", }) data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldYearMonth, + VerificationField: data.VerificationTypeYearMonth, VerificationValue: "2000-01", }) data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldPin, + VerificationField: data.VerificationTypePin, VerificationValue: "8901", }) data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldNationalID, + VerificationField: data.VerificationTypeNationalID, VerificationValue: "OLDID890", }) @@ -498,27 +498,27 @@ func Test_UpdateReceiverHandler(t *testing.T) { ` receiverVerifications := []struct { - verificationField data.VerificationField + verificationField data.VerificationType newVerificationValue string oldVerificationValue string }{ { - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, newVerificationValue: "1999-01-01", oldVerificationValue: "2000-01-01", }, { - verificationField: data.VerificationFieldYearMonth, + verificationField: data.VerificationTypeYearMonth, newVerificationValue: "1999-01", oldVerificationValue: "2000-01", }, { - verificationField: data.VerificationFieldPin, + verificationField: data.VerificationTypePin, newVerificationValue: "1234", oldVerificationValue: "8901", }, { - verificationField: data.VerificationFieldNationalID, + verificationField: data.VerificationTypeNationalID, newVerificationValue: "NEWID123", oldVerificationValue: "OLDID890", }, @@ -571,27 +571,27 @@ func Test_UpdateReceiverHandler(t *testing.T) { ` receiverVerifications := []struct { - verificationField data.VerificationField + verificationField data.VerificationType newVerificationValue string oldVerificationValue string }{ { - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, newVerificationValue: "1999-01-01", oldVerificationValue: "2000-01-01", }, { - verificationField: data.VerificationFieldYearMonth, + verificationField: data.VerificationTypeYearMonth, newVerificationValue: "1999-01", oldVerificationValue: "", }, { - verificationField: data.VerificationFieldPin, + verificationField: data.VerificationTypePin, newVerificationValue: "1234", oldVerificationValue: "", }, { - verificationField: data.VerificationFieldNationalID, + verificationField: data.VerificationTypeNationalID, newVerificationValue: "NEWID123", oldVerificationValue: "", }, diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler.go b/internal/serve/httphandler/verifiy_receiver_registration_handler.go index 9b16214cd..a6fe3a341 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler.go @@ -119,16 +119,16 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( truncatedPhoneNumber := utils.TruncateString(receiver.PhoneNumber, 3) // STEP 1: find the receiverVerification entry that matches the pair [receiverID, verificationType] - receiverVerifications, err := v.Models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{receiver.ID}, receiverRegistrationRequest.VerificationType) + receiverVerifications, err := v.Models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{receiver.ID}, receiverRegistrationRequest.VerificationField) if err != nil { - return fmt.Errorf("error retrieving receiver verification for verification type %s: %w", receiverRegistrationRequest.VerificationType, err) + return fmt.Errorf("error retrieving receiver verification for verification type %s: %w", receiverRegistrationRequest.VerificationField, err) } if len(receiverVerifications) == 0 { - err = fmt.Errorf("%s not found for receiver with phone number %s", receiverRegistrationRequest.VerificationType, truncatedPhoneNumber) + err = fmt.Errorf("%s not found for receiver with phone number %s", receiverRegistrationRequest.VerificationField, truncatedPhoneNumber) return &ErrorInformationNotFound{cause: err} } if len(receiverVerifications) > 1 { - log.Ctx(ctx).Warnf("receiver with id %s has more than one verification saved in the database for type %s", receiver.ID, receiverRegistrationRequest.VerificationType) + log.Ctx(ctx).Warnf("receiver with id %s has more than one verification saved in the database for type %s", receiver.ID, receiverRegistrationRequest.VerificationField) } receiverVerification := receiverVerifications[0] @@ -155,7 +155,7 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( } if !data.CompareVerificationValue(receiverVerification.HashedValue, receiverRegistrationRequest.VerificationValue) { - baseErrMsg := fmt.Sprintf("%s value does not match for user with phone number %s", receiverRegistrationRequest.VerificationType, truncatedPhoneNumber) + baseErrMsg := fmt.Sprintf("%s value does not match for user with phone number %s", receiverRegistrationRequest.VerificationField, truncatedPhoneNumber) // update the receiver verification with the confirmation that the value was checked rvu.Attempts = utils.IntPtr(receiverVerification.Attempts + 1) rvu.FailedAt = &now diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go index a842a0d2c..b163aadcc 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go @@ -102,7 +102,7 @@ func Test_VerifyReceiverRegistrationHandler_validate(t *testing.T) { "phone_number": "+380445555555", "otp": "", "verification": "1990-01-01", - "verification_type": "date_of_birth", + "verification_field": "date_of_birth", "reCAPTCHA_token": "token" }`, isRecaptchaValidFnResponse: []interface{}{true, nil}, @@ -115,7 +115,7 @@ func Test_VerifyReceiverRegistrationHandler_validate(t *testing.T) { "phone_number": "+380445555555", "otp": "123456", "verification": "1990-01-01", - "verification_type": "date_of_birth", + "verification_field": "date_of_birth", "reCAPTCHA_token": "token" }`, isRecaptchaValidFnResponse: []interface{}{true, nil}, @@ -124,7 +124,7 @@ func Test_VerifyReceiverRegistrationHandler_validate(t *testing.T) { PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "1990-01-01", - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, ReCAPTCHAToken: "token", }, }, @@ -205,13 +205,13 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiverWithExceededAttempts := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: "+380446666666"}) receiverVerificationExceededAttempts := data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiverWithExceededAttempts.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) receiverVerificationExceededAttempts.Attempts = data.MaxAttemptsAllowed err = models.ReceiverVerification.UpdateReceiverVerification(ctx, data.ReceiverVerificationUpdate{ ReceiverID: receiverWithExceededAttempts.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, Attempts: utils.IntPtr(data.MaxAttemptsAllowed), VerificationChannel: message.MessageChannelSMS, }, dbConnectionPool) @@ -221,7 +221,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: "+380445555555"}) _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) @@ -240,7 +240,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiver: *receiverMissingReceiverVerification, registrationRequest: data.ReceiverRegistrationRequest{ PhoneNumber: receiverMissingReceiverVerification.PhoneNumber, - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }, wantErrContains: "DATE_OF_BIRTH not found for receiver with phone number +38...333", @@ -250,7 +250,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiver: *receiver, registrationRequest: data.ReceiverRegistrationRequest{ PhoneNumber: receiver.PhoneNumber, - VerificationType: data.VerificationFieldYearMonth, + VerificationField: data.VerificationTypeYearMonth, VerificationValue: "1999-12", }, wantErrContains: "YEAR_MONTH not found for receiver with phone number +38...555", @@ -260,7 +260,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiver: *receiver, registrationRequest: data.ReceiverRegistrationRequest{ PhoneNumber: receiver.PhoneNumber, - VerificationType: data.VerificationFieldNationalID, + VerificationField: data.VerificationTypeNationalID, VerificationValue: "123456", }, wantErrContains: "NATIONAL_ID_NUMBER not found for receiver with phone number +38...555", @@ -270,7 +270,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiver: *receiverWithExceededAttempts, registrationRequest: data.ReceiverRegistrationRequest{ PhoneNumber: receiverWithExceededAttempts.PhoneNumber, - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }, wantErrContains: "the number of attempts to confirm the verification value exceededs the max attempts", @@ -280,7 +280,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiver: *receiver, registrationRequest: data.ReceiverRegistrationRequest{ PhoneNumber: receiver.PhoneNumber, - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-11-11", // <--- different from the DB one (1990-01-01) }, shouldAssertAttemptsCount: true, @@ -291,7 +291,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te receiver: *receiver, registrationRequest: data.ReceiverRegistrationRequest{ PhoneNumber: receiver.PhoneNumber, - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }, shouldAssertAttemptsCount: true, @@ -310,7 +310,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te var receiverVerifications []*data.ReceiverVerification var receiverVerificationInitial *data.ReceiverVerification if tc.shouldAssertAttemptsCount { - receiverVerifications, err = models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{tc.receiver.ID}, tc.registrationRequest.VerificationType) + receiverVerifications, err = models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{tc.receiver.ID}, tc.registrationRequest.VerificationField) require.NoError(t, err) require.Len(t, receiverVerifications, 1) receiverVerificationInitial = receiverVerifications[0] @@ -319,7 +319,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te err = handler.processReceiverVerificationPII(ctx, dbTx, tc.receiver, tc.registrationRequest) if tc.wantErrContains == "" { - receiverVerifications, err = models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{tc.receiver.ID}, tc.registrationRequest.VerificationType) + receiverVerifications, err = models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{tc.receiver.ID}, tc.registrationRequest.VerificationField) require.NoError(t, err) require.Len(t, receiverVerifications, 1) receiverVerification := receiverVerifications[0] @@ -328,7 +328,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te } else { require.ErrorContains(t, err, tc.wantErrContains) if tc.shouldAssertAttemptsCount { - receiverVerifications, err = models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{tc.receiver.ID}, tc.registrationRequest.VerificationType) + receiverVerifications, err = models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{tc.receiver.ID}, tc.registrationRequest.VerificationField) require.NoError(t, err) require.Len(t, receiverVerifications, 1) receiverVerification := receiverVerifications[0] @@ -779,7 +779,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin PhoneNumber: phoneNumber, OTP: "123456", VerificationValue: "1990-01-01", - VerificationType: "date_of_birth", + VerificationField: "date_of_birth", ReCAPTCHAToken: "token", } reqBody, err := json.Marshal(receiverRegistrationRequest) @@ -905,13 +905,13 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin receiverWithExceededAttempts := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) receiverVerificationExceededAttempts := data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiverWithExceededAttempts.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) receiverVerificationExceededAttempts.Attempts = data.MaxAttemptsAllowed err = models.ReceiverVerification.UpdateReceiverVerification(ctx, data.ReceiverVerificationUpdate{ ReceiverID: receiverWithExceededAttempts.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, Attempts: utils.IntPtr(data.MaxAttemptsAllowed), VerificationChannel: message.MessageChannelSMS, }, dbConnectionPool) @@ -960,7 +960,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) @@ -1020,7 +1020,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) @@ -1100,7 +1100,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) @@ -1205,7 +1205,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) @@ -1323,7 +1323,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) diff --git a/internal/serve/publicfiles/js/receiver_registration.js b/internal/serve/publicfiles/js/receiver_registration.js index 566ea90cc..1e42b3017 100644 --- a/internal/serve/publicfiles/js/receiver_registration.js +++ b/internal/serve/publicfiles/js/receiver_registration.js @@ -304,7 +304,7 @@ async function handleVerificationInfoSubmitted() { [contactMethod]: contactValue, otp: otp, recaptcha_token: reCAPTCHAToken, - verification_type: WalletRegistration.verificationField, + verification_field: WalletRegistration.verificationField, verification: verificationFieldValue, }), }); diff --git a/internal/serve/validators/disbursement_instructions_validator.go b/internal/serve/validators/disbursement_instructions_validator.go index 2c9e85b45..63bc492b3 100644 --- a/internal/serve/validators/disbursement_instructions_validator.go +++ b/internal/serve/validators/disbursement_instructions_validator.go @@ -9,11 +9,11 @@ import ( ) type DisbursementInstructionsValidator struct { - verificationField data.VerificationField + verificationField data.VerificationType *Validator } -func NewDisbursementInstructionsValidator(verificationField data.VerificationField) *DisbursementInstructionsValidator { +func NewDisbursementInstructionsValidator(verificationField data.VerificationType) *DisbursementInstructionsValidator { return &DisbursementInstructionsValidator{ verificationField: verificationField, Validator: NewValidator(), @@ -40,13 +40,13 @@ func (iv *DisbursementInstructionsValidator) ValidateInstruction(instruction *da // validate verification field switch iv.verificationField { - case data.VerificationFieldDateOfBirth: + case data.VerificationTypeDateOfBirth: iv.CheckError(utils.ValidateDateOfBirthVerification(verification), fmt.Sprintf("line %d - date of birth", lineNumber), "") - case data.VerificationFieldYearMonth: + case data.VerificationTypeYearMonth: iv.CheckError(utils.ValidateYearMonthVerification(verification), fmt.Sprintf("line %d - year/month", lineNumber), "") - case data.VerificationFieldPin: + case data.VerificationTypePin: iv.CheckError(utils.ValidatePinVerification(verification), fmt.Sprintf("line %d - pin", lineNumber), "") - case data.VerificationFieldNationalID: + case data.VerificationTypeNationalID: iv.CheckError(utils.ValidateNationalIDVerification(verification), fmt.Sprintf("line %d - national id", lineNumber), "") } } diff --git a/internal/serve/validators/disbursement_instructions_validator_test.go b/internal/serve/validators/disbursement_instructions_validator_test.go index f577c79a8..36c4deefd 100644 --- a/internal/serve/validators/disbursement_instructions_validator_test.go +++ b/internal/serve/validators/disbursement_instructions_validator_test.go @@ -13,7 +13,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing name string instruction *data.DisbursementInstruction lineNumber int - verificationField data.VerificationField + verificationField data.VerificationType hasErrors bool expectedErrors map[string]interface{} }{ @@ -25,7 +25,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990-01-01", }, lineNumber: 2, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 2 - phone": "phone cannot be empty", @@ -35,7 +35,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing name: "error with all fields empty (phone, id, amount, date of birth)", instruction: &data.DisbursementInstruction{}, lineNumber: 2, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 2 - amount": "invalid amount. Amount must be a positive number", @@ -53,7 +53,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990-01-01", }, lineNumber: 2, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 2 - phone": "invalid phone format. Correct format: +380445555555", @@ -68,7 +68,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990-01-01", }, lineNumber: 3, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - amount": "invalid amount. Amount must be a positive number", @@ -83,7 +83,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990-01-01", }, lineNumber: 3, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - amount": "invalid amount. Amount must be a positive number", @@ -98,7 +98,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990/01/01", }, lineNumber: 3, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - date of birth": "invalid date of birth format. Correct format: 1990-01-30", @@ -113,7 +113,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "2090-01-01", }, lineNumber: 3, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - date of birth": "date of birth cannot be in the future", @@ -128,7 +128,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990/01", }, lineNumber: 3, - verificationField: data.VerificationFieldYearMonth, + verificationField: data.VerificationTypeYearMonth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - year/month": "invalid year/month format. Correct format: 1990-12", @@ -143,7 +143,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "2090-01", }, lineNumber: 3, - verificationField: data.VerificationFieldYearMonth, + verificationField: data.VerificationTypeYearMonth, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - year/month": "year/month cannot be in the future", @@ -158,7 +158,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "123", }, lineNumber: 3, - verificationField: data.VerificationFieldPin, + verificationField: data.VerificationTypePin, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - pin": "invalid pin length. Cannot have less than 4 or more than 8 characters in pin", @@ -173,7 +173,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "123456789", }, lineNumber: 3, - verificationField: data.VerificationFieldPin, + verificationField: data.VerificationTypePin, hasErrors: true, expectedErrors: map[string]interface{}{ "line 3 - pin": "invalid pin length. Cannot have less than 4 or more than 8 characters in pin", @@ -188,7 +188,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "6UZMB56FWTKV4U0PJ21TBR6VOQVYSGIMZG2HW2S0L7EK5K83W78", }, lineNumber: 3, - verificationField: data.VerificationFieldNationalID, + 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", @@ -204,7 +204,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990-01-01", }, lineNumber: 1, - verificationField: data.VerificationFieldDateOfBirth, + verificationField: data.VerificationTypeDateOfBirth, hasErrors: false, }, { @@ -216,7 +216,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1990-01", }, lineNumber: 1, - verificationField: data.VerificationFieldYearMonth, + verificationField: data.VerificationTypeYearMonth, hasErrors: false, }, { @@ -228,7 +228,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "ABCD123", }, lineNumber: 3, - verificationField: data.VerificationFieldNationalID, + verificationField: data.VerificationTypeNationalID, hasErrors: false, }, { @@ -240,7 +240,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing VerificationValue: "1234", }, lineNumber: 3, - verificationField: data.VerificationFieldPin, + verificationField: data.VerificationTypePin, hasErrors: false, }, } @@ -304,7 +304,7 @@ func Test_DisbursementInstructionsValidator_SanitizeInstruction(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - iv := NewDisbursementInstructionsValidator(data.VerificationFieldDateOfBirth) + iv := NewDisbursementInstructionsValidator(data.VerificationTypeDateOfBirth) sanitizedInstruction := iv.SanitizeInstruction(tt.actual) assert.Equal(t, tt.expectedInstruction, sanitizedInstruction) diff --git a/internal/serve/validators/disbursement_request_validator.go b/internal/serve/validators/disbursement_request_validator.go index f93721308..ea856a666 100644 --- a/internal/serve/validators/disbursement_request_validator.go +++ b/internal/serve/validators/disbursement_request_validator.go @@ -8,11 +8,11 @@ import ( ) type DisbursementRequestValidator struct { - verificationField data.VerificationField + verificationField data.VerificationType *Validator } -func NewDisbursementRequestValidator(verificationField data.VerificationField) *DisbursementRequestValidator { +func NewDisbursementRequestValidator(verificationField data.VerificationType) *DisbursementRequestValidator { return &DisbursementRequestValidator{ verificationField: verificationField, Validator: NewValidator(), @@ -20,9 +20,9 @@ func NewDisbursementRequestValidator(verificationField data.VerificationField) * } // ValidateAndGetVerificationType validates if the verification type field is a valid value. -func (dv *DisbursementRequestValidator) ValidateAndGetVerificationType() data.VerificationField { - if !slices.Contains(data.GetAllVerificationFields(), dv.verificationField) { - dv.Check(false, "verification_field", fmt.Sprintf("invalid parameter. valid values are: %v", data.GetAllVerificationFields())) +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 index 8d65be8cf..5c5e15399 100644 --- a/internal/serve/validators/disbursement_request_validator_test.go +++ b/internal/serve/validators/disbursement_request_validator_test.go @@ -10,11 +10,11 @@ import ( func Test_DisbursementRequestValidator_ValidateAndGetVerificationType(t *testing.T) { t.Run("Valid verification type", func(t *testing.T) { - validField := []data.VerificationField{ - data.VerificationFieldDateOfBirth, - data.VerificationFieldYearMonth, - data.VerificationFieldPin, - data.VerificationFieldNationalID, + validField := []data.VerificationType{ + data.VerificationTypeDateOfBirth, + data.VerificationTypeYearMonth, + data.VerificationTypePin, + data.VerificationTypeNationalID, } for _, field := range validField { validator := NewDisbursementRequestValidator(field) @@ -23,7 +23,7 @@ func Test_DisbursementRequestValidator_ValidateAndGetVerificationType(t *testing }) t.Run("Invalid verification type", func(t *testing.T) { - field := data.VerificationField("field") + field := data.VerificationType("field") validator := NewDisbursementRequestValidator(field) actual := validator.ValidateAndGetVerificationType() diff --git a/internal/serve/validators/receiver_registration_validator.go b/internal/serve/validators/receiver_registration_validator.go index 1a92f0455..0fdf20367 100644 --- a/internal/serve/validators/receiver_registration_validator.go +++ b/internal/serve/validators/receiver_registration_validator.go @@ -26,7 +26,7 @@ func (rv *ReceiverRegistrationValidator) ValidateReceiver(receiverInfo *data.Rec phone := strings.TrimSpace(receiverInfo.PhoneNumber) otp := strings.TrimSpace(receiverInfo.OTP) verification := strings.TrimSpace(receiverInfo.VerificationValue) - verificationType := strings.TrimSpace(string(receiverInfo.VerificationType)) + verificationField := strings.TrimSpace(string(receiverInfo.VerificationField)) // validate phone field rv.CheckError(utils.ValidatePhoneNumber(phone), "phone_number", "invalid phone format. Correct format: +380445555555") @@ -36,33 +36,33 @@ func (rv *ReceiverRegistrationValidator) ValidateReceiver(receiverInfo *data.Rec rv.CheckError(utils.ValidateOTP(otp), "otp", "invalid otp format. Needs to be a 6 digit value") // validate verification type field - rv.Check(verificationType != "", "verification_type", "verification type cannot be empty") - vt := rv.validateAndGetVerificationType(verificationType) + rv.Check(verificationField != "", "verification_field", "verification type cannot be empty") + vf := rv.validateAndGetVerificationType(verificationField) // validate verification fields - switch vt { - case data.VerificationFieldDateOfBirth: + switch vf { + case data.VerificationTypeDateOfBirth: rv.CheckError(utils.ValidateDateOfBirthVerification(verification), "verification", "") - case data.VerificationFieldYearMonth: + case data.VerificationTypeYearMonth: rv.CheckError(utils.ValidateYearMonthVerification(verification), "verification", "") - case data.VerificationFieldPin: + case data.VerificationTypePin: rv.CheckError(utils.ValidatePinVerification(verification), "verification", "") - case data.VerificationFieldNationalID: + case data.VerificationTypeNationalID: rv.CheckError(utils.ValidateNationalIDVerification(verification), "verification", "") } receiverInfo.PhoneNumber = phone receiverInfo.OTP = otp receiverInfo.VerificationValue = verification - receiverInfo.VerificationType = vt + receiverInfo.VerificationField = vf } // validateAndGetVerificationType validates if the verification type field is a valid value. -func (rv *ReceiverRegistrationValidator) validateAndGetVerificationType(verificationType string) data.VerificationField { - vt := data.VerificationField(strings.ToUpper(verificationType)) +func (rv *ReceiverRegistrationValidator) validateAndGetVerificationType(verificationType string) data.VerificationType { + vt := data.VerificationType(strings.ToUpper(verificationType)) - if !slices.Contains(data.GetAllVerificationFields(), vt) { - rv.Check(false, "verification_type", fmt.Sprintf("invalid parameter. valid values are: %v", data.GetAllVerificationFields())) + if !slices.Contains(data.GetAllVerificationTypes(), vt) { + rv.Check(false, "verification_field", fmt.Sprintf("invalid parameter. valid values are: %v", data.GetAllVerificationTypes())) return "" } return vt diff --git a/internal/serve/validators/receiver_registration_validator_test.go b/internal/serve/validators/receiver_registration_validator_test.go index b338f3bd7..0376c16e7 100644 --- a/internal/serve/validators/receiver_registration_validator_test.go +++ b/internal/serve/validators/receiver_registration_validator_test.go @@ -25,7 +25,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "invalid", OTP: "123456", VerificationValue: "1990-01-01", - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }, expectedErrorLen: 1, expectedErrorMsg: "invalid phone format. Correct format: +380445555555", @@ -37,7 +37,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "", OTP: "123456", VerificationValue: "1990-01-01", - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }, expectedErrorLen: 1, expectedErrorMsg: "phone cannot be empty", @@ -49,7 +49,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555", OTP: "12mock", VerificationValue: "1990-01-01", - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }, expectedErrorLen: 1, expectedErrorMsg: "invalid otp format. Needs to be a 6 digit value", @@ -61,11 +61,11 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "1990-01-01", - VerificationType: "mock_type", + VerificationField: "mock_type", }, expectedErrorLen: 1, expectedErrorMsg: "invalid parameter. valid values are: [DATE_OF_BIRTH YEAR_MONTH PIN NATIONAL_ID_NUMBER]", - expectedErrorKey: "verification_type", + expectedErrorKey: "verification_field", }, { name: "error if verification[DATE_OF_BIRTH] is invalid", @@ -73,7 +73,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "90/01/01", - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }, expectedErrorLen: 1, expectedErrorMsg: "invalid date of birth format. Correct format: 1990-01-30", @@ -85,7 +85,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "90/12", - VerificationType: data.VerificationFieldYearMonth, + VerificationField: data.VerificationTypeYearMonth, }, expectedErrorLen: 1, expectedErrorMsg: "invalid year/month format. Correct format: 1990-12", @@ -97,7 +97,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "ABCDE1234", - VerificationType: data.VerificationFieldPin, + VerificationField: data.VerificationTypePin, }, expectedErrorLen: 1, expectedErrorMsg: "invalid pin length. Cannot have less than 4 or more than 8 characters in pin", @@ -109,7 +109,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "6UZMB56FWTKV4U0PJ21TBR6VOQVYSGIMZG2HW2S0L7EK5K83W78XXXXX", - VerificationType: data.VerificationFieldNationalID, + VerificationField: data.VerificationTypeNationalID, }, expectedErrorLen: 1, expectedErrorMsg: "invalid national id. Cannot have more than 50 characters in national id", @@ -121,14 +121,14 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555 ", OTP: " 123456 ", VerificationValue: "1990-01-01 ", - VerificationType: "date_of_birth", + VerificationField: "date_of_birth", }, expectedErrorLen: 0, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "1990-01-01", - VerificationType: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }, }, { @@ -137,14 +137,14 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555 ", OTP: " 123456 ", VerificationValue: "1990-12 ", - VerificationType: "year_month", + VerificationField: "year_month", }, expectedErrorLen: 0, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "1990-12", - VerificationType: data.VerificationFieldYearMonth, + VerificationField: data.VerificationTypeYearMonth, }, }, { @@ -153,14 +153,14 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555 ", OTP: " 123456 ", VerificationValue: "1234 ", - VerificationType: "pin", + VerificationField: "pin", }, expectedErrorLen: 0, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "1234", - VerificationType: data.VerificationFieldPin, + VerificationField: data.VerificationTypePin, }, }, { @@ -169,14 +169,14 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { PhoneNumber: "+380445555555 ", OTP: " 123456 ", VerificationValue: " NATIONALIDNUMBER123", - VerificationType: "national_id_number", + VerificationField: "national_id_number", }, expectedErrorLen: 0, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", VerificationValue: "NATIONALIDNUMBER123", - VerificationType: data.VerificationFieldNationalID, + VerificationField: data.VerificationTypeNationalID, }, }, } @@ -194,7 +194,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { assert.Equal(t, tc.expectedReceiver.PhoneNumber, tc.receiverInfo.PhoneNumber) assert.Equal(t, tc.expectedReceiver.OTP, tc.receiverInfo.OTP) assert.Equal(t, tc.expectedReceiver.VerificationValue, tc.receiverInfo.VerificationValue) - assert.Equal(t, tc.expectedReceiver.VerificationType, tc.receiverInfo.VerificationType) + assert.Equal(t, tc.expectedReceiver.VerificationField, tc.receiverInfo.VerificationField) } }) } @@ -203,11 +203,11 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { func Test_ReceiverRegistrationValidator_ValidateAndGetVerificationType(t *testing.T) { t.Run("Valid verification type", func(t *testing.T) { validator := NewReceiverRegistrationValidator() - validField := []data.VerificationField{ - data.VerificationFieldDateOfBirth, - data.VerificationFieldYearMonth, - data.VerificationFieldPin, - data.VerificationFieldNationalID, + validField := []data.VerificationType{ + data.VerificationTypeDateOfBirth, + data.VerificationTypeYearMonth, + data.VerificationTypePin, + data.VerificationTypeNationalID, } for _, field := range validField { assert.Equal(t, field, validator.validateAndGetVerificationType(string(field))) @@ -221,6 +221,6 @@ func Test_ReceiverRegistrationValidator_ValidateAndGetVerificationType(t *testin actual := validator.validateAndGetVerificationType(invalidStatus) 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_type"]) + assert.Equal(t, "invalid parameter. valid values are: [DATE_OF_BIRTH YEAR_MONTH PIN NATIONAL_ID_NUMBER]", validator.Errors["verification_field"]) }) } diff --git a/internal/services/patch_anchor_platform_transactions_completion_test.go b/internal/services/patch_anchor_platform_transactions_completion_test.go index d078d3a14..8f52101cc 100644 --- a/internal/services/patch_anchor_platform_transactions_completion_test.go +++ b/internal/services/patch_anchor_platform_transactions_completion_test.go @@ -78,7 +78,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -115,7 +115,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -186,7 +186,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -240,7 +240,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -305,7 +305,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -370,7 +370,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionForP Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -454,7 +454,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -518,7 +518,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -578,7 +578,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -641,7 +641,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ @@ -649,7 +649,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -725,7 +725,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) disbursement2 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ @@ -733,7 +733,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) disbursement3 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ @@ -741,7 +741,7 @@ func Test_PatchAnchorPlatformTransactionCompletionService_PatchAPTransactionsFor Wallet: wallet, Asset: asset, Status: data.StartedDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) payment1 := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ diff --git a/internal/services/ready_payments_cancelation_service_test.go b/internal/services/ready_payments_cancelation_service_test.go index b35dcb3c2..834ea3622 100644 --- a/internal/services/ready_payments_cancelation_service_test.go +++ b/internal/services/ready_payments_cancelation_service_test.go @@ -48,7 +48,7 @@ func Test_ReadyPaymentsCancellationService_CancelReadyPaymentsService(t *testing Wallet: wallet, Asset: asset, Status: data.ReadyDisbursementStatus, - VerificationField: data.VerificationFieldDateOfBirth, + VerificationField: data.VerificationTypeDateOfBirth, }) t.Run("automatic payment cancellation is deactivated", func(t *testing.T) { From a1720e09a9e7859bff120a1531678fad2af7ce87 Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Thu, 5 Sep 2024 15:58:04 -0700 Subject: [PATCH 22/75] [SDP-1308] Update process disbursement instructions to accept email/phoneNumber (#404) --- ...-09-04.0-alter-receivers-table-indexes.sql | 34 ++ internal/data/assets.go | 4 +- internal/data/disbursement_instructions.go | 389 ++++++++++++------ .../data/disbursement_instructions_test.go | 230 +++++++++-- internal/data/disbursement_receivers.go | 2 +- internal/data/disbursements_test.go | 6 +- internal/data/fixtures.go | 5 +- internal/data/fixtures_test.go | 10 +- internal/data/receivers.go | 114 +++-- internal/data/receivers_test.go | 117 ++++-- internal/data/receivers_wallet.go | 4 +- internal/integrationtests/utils_test.go | 9 +- ...eceiver_wallets_sms_invitation_job_test.go | 2 + .../serve/httphandler/disbursement_handler.go | 63 ++- .../httphandler/disbursement_handler_test.go | 26 +- .../httphandler/receiver_handler_test.go | 19 +- .../httphandler/update_receiver_handler.go | 14 +- .../update_receiver_handler_test.go | 16 +- .../verifiy_receiver_registration_handler.go | 3 +- ...ifiy_receiver_registration_handler_test.go | 4 +- .../disbursement_instructions_validator.go | 35 +- ...isbursement_instructions_validator_test.go | 66 ++- .../send_receiver_wallets_invite_service.go | 39 +- ...nd_receiver_wallets_invite_service_test.go | 25 ++ 24 files changed, 922 insertions(+), 314 deletions(-) create mode 100644 db/migrations/sdp-migrations/2024-09-04.0-alter-receivers-table-indexes.sql diff --git a/db/migrations/sdp-migrations/2024-09-04.0-alter-receivers-table-indexes.sql b/db/migrations/sdp-migrations/2024-09-04.0-alter-receivers-table-indexes.sql new file mode 100644 index 000000000..54f92bab9 --- /dev/null +++ b/db/migrations/sdp-migrations/2024-09-04.0-alter-receivers-table-indexes.sql @@ -0,0 +1,34 @@ +-- +migrate Up +-- Remove existing unique constraint on phone_number +ALTER TABLE receivers DROP CONSTRAINT IF EXISTS payments_account_phone_number_key; + +DROP INDEX IF EXISTS payments_account_phone_number_key; + +-- Make phone_number nullable +ALTER TABLE receivers ALTER COLUMN phone_number DROP NOT NULL; + +-- Add check constraint to ensure at least one contact method is provided +ALTER TABLE receivers ADD CONSTRAINT receiver_contact_check + CHECK (phone_number IS NOT NULL OR email IS NOT NULL); + +-- Create unique indexes on both phone_number and email separately +CREATE UNIQUE INDEX receiver_unique_phone_number ON receivers (LOWER(phone_number)) + WHERE phone_number IS NOT NULL; + +CREATE UNIQUE INDEX receiver_unique_email ON receivers (LOWER(email)) + WHERE email IS NOT NULL; + +-- +migrate Down +-- Remove the new check constraint +ALTER TABLE receivers DROP CONSTRAINT IF EXISTS receiver_contact_check; + +-- Remove the new unique indexes +DROP INDEX IF EXISTS receiver_unique_phone_number; +DROP INDEX IF EXISTS receiver_unique_email; + +-- Restore phone_number to NOT NULL +UPDATE receivers SET phone_number = 'UNKNOWN' WHERE phone_number IS NULL; +ALTER TABLE receivers ALTER COLUMN phone_number SET NOT NULL; + +-- Recreate the original unique constraint and index on phone_number +ALTER TABLE receivers ADD CONSTRAINT payments_account_phone_number_key UNIQUE (phone_number); \ No newline at end of file diff --git a/internal/data/assets.go b/internal/data/assets.go index 723f80df5..3bc5f6071 100644 --- a/internal/data/assets.go +++ b/internal/data/assets.go @@ -284,8 +284,8 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal rw.invitation_sent_at AS "receiver_wallet.invitation_sent_at", COALESCE(mrsi.total_invitation_sms_resent_attempts, 0) AS "receiver_wallet.total_invitation_sms_resent_attempts", r.id AS "receiver_wallet.receiver.id", - r.phone_number AS "receiver_wallet.receiver.phone_number", - r.email AS "receiver_wallet.receiver.email", + COALESCE(r.phone_number, '') AS "receiver_wallet.receiver.phone_number", + COALESCE(r.email, '') AS "receiver_wallet.receiver.email", a.id AS "asset.id", a.code AS "asset.code", a.issuer AS "asset.issuer", diff --git a/internal/data/disbursement_instructions.go b/internal/data/disbursement_instructions.go index 2281a2df4..94e8247f5 100644 --- a/internal/data/disbursement_instructions.go +++ b/internal/data/disbursement_instructions.go @@ -5,15 +5,31 @@ import ( "errors" "fmt" + "golang.org/x/exp/maps" + "github.com/stellar/stellar-disbursement-platform-backend/db" ) type DisbursementInstruction struct { - Phone string `csv:"phone"` - ID string `csv:"id"` - Amount string `csv:"amount"` - VerificationValue string `csv:"verification"` - ExternalPaymentId *string `csv:"paymentID"` + Phone string `csv:"phone"` + Email string `csv:"email"` + ID string `csv:"id"` + Amount string `csv:"amount"` + VerificationValue string `csv:"verification"` + ExternalPaymentId string `csv:"paymentID"` +} + +func (di *DisbursementInstruction) Contact() (string, error) { + if di.Phone != "" && di.Email != "" { + return "", errors.New("phone and email are both provided") + } + if di.Phone != "" { + return di.Phone, nil + } + if di.Email != "" { + return di.Email, nil + } + return "", errors.New("phone and email are empty") } type DisbursementInstructionModel struct { @@ -25,11 +41,6 @@ type DisbursementInstructionModel struct { disbursementModel *DisbursementModel } -type InstructionLine struct { - line int - disbursementInstruction *DisbursementInstruction -} - const MaxInstructionsPerDisbursement = 10000 // NewDisbursementInstructionModel creates a new DisbursementInstructionModel. @@ -49,10 +60,19 @@ var ( ErrReceiverVerificationMismatch = errors.New("receiver verification mismatch") ) +type DisbursementInstructionsOpts struct { + UserID string + Instructions []*DisbursementInstruction + ReceiverContactType ReceiverContactType + Disbursement *Disbursement + DisbursementUpdate *DisbursementUpdate + MaxNumberOfInstructions int +} + // ProcessAll Processes all disbursement instructions and persists the data to the database. // -// |--- For each phone number in the instructions: -// | |--- Check if a receiver exists. +// |--- For each line in the instructions: +// | |--- Check if a receiver exists by their contact information (phone, email). // | | |--- If a receiver does not exist, create one. // | |--- For each receiver: // | | |--- Check if the receiver verification exists. @@ -67,166 +87,267 @@ var ( // | | | |--- If the receiver wallet exists and it's not REGISTERED, retry the invitation SMS. // | | |--- Delete all previously existing payments tied to this disbursement. // | | |--- Create all payments passed in the instructions. -func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, userID string, instructions []*DisbursementInstruction, disbursement *Disbursement, update *DisbursementUpdate, maxNumberOfInstructions int) error { - if len(instructions) > maxNumberOfInstructions { +func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, opts DisbursementInstructionsOpts) error { + if len(opts.Instructions) > opts.MaxNumberOfInstructions { return ErrMaxInstructionsExceeded } // 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 phone number and create missing ones - phoneNumbers := make([]string, 0, len(instructions)) - for _, instruction := range instructions { - phoneNumbers = append(phoneNumbers, instruction.Phone) + // 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) + if err != nil { + return fmt.Errorf("processing receivers: %w", err) } - existingReceivers, err := di.receiverModel.GetByPhoneNumbers(ctx, dbTx, phoneNumbers) + // 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("error fetching receivers by phone number: %w", err) + return fmt.Errorf("processing receiver verifications: %w", err) } - // Create a map of existing receivers for easy lookup - receiverMap := make(map[string]*Receiver) - for _, receiver := range existingReceivers { - receiverMap[receiver.PhoneNumber] = receiver + // Step 3: 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) } - instructionMap := make(map[string]InstructionLine) - for line, instruction := range instructions { - instructionMap[instruction.Phone] = InstructionLine{ - line: line + 1, - disbursementInstruction: instruction, - } + // 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) } - // Create missing receivers - for _, instruction := range instructions { - _, exists := receiverMap[instruction.Phone] - if !exists { - receiverInsert := ReceiverInsert{ - PhoneNumber: instruction.Phone, - ExternalId: &instruction.ID, - } - receiver, insertErr := di.receiverModel.Insert(ctx, dbTx, receiverInsert) - if insertErr != nil { - return fmt.Errorf("error inserting receiver: %w", insertErr) - } - receiverMap[instruction.Phone] = receiver - } + // Step 5: Create payments for all receivers + if err = di.createPayments(ctx, dbTx, receiversByIDMap, receiverIDToReceiverWalletIDMap, opts.Instructions, opts.Disbursement); err != nil { + return fmt.Errorf("creating payments: %w", err) } - // Step 2: Fetch all receiver verifications and create missing ones. - receiverIDs := make([]string, 0, len(receiverMap)) - for _, receiver := range receiverMap { - receiverIDs = append(receiverIDs, receiver.ID) + // Step 6: Persist Payment file to Disbursement + if err = di.disbursementModel.Update(ctx, opts.DisbursementUpdate); err != nil { + return fmt.Errorf("persisting payment file: %w", err) } - verifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbTx, receiverIDs, disbursement.VerificationField) - if err != nil { - return fmt.Errorf("error fetching receiver verifications: %w", err) + + // Step 7: Update Disbursement Status + if err = di.disbursementModel.UpdateStatus(ctx, dbTx, opts.UserID, opts.Disbursement.ID, ReadyDisbursementStatus); err != nil { + return fmt.Errorf("updating status: %w", err) } - verificationMap := make(map[string]*ReceiverVerification) - for _, verification := range verifications { - verificationMap[verification.ReceiverID] = verification + return 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 + contacts := make([]string, 0, len(instructions)) + for _, instruction := range instructions { + contact, err := instruction.Contact() + if err != nil { + return nil, fmt.Errorf("resolving contact information for instruction with ID %s: %w", instruction.ID, err) } + contacts = append(contacts, contact) + } - for _, receiver := range receiverMap { - verification, verificationExists := verificationMap[receiver.ID] - instruction := instructionMap[receiver.PhoneNumber] - if !verificationExists { - verificationInsert := ReceiverVerificationInsert{ - ReceiverID: receiver.ID, - VerificationValue: instruction.disbursementInstruction.VerificationValue, - VerificationField: disbursement.VerificationField, - } - hashedVerification, insertError := di.receiverVerificationModel.Insert(ctx, dbTx, verificationInsert) - if insertError != nil { - return fmt.Errorf("error inserting receiver verification: %w", insertError) - } - verificationMap[receiver.ID] = &ReceiverVerification{ - ReceiverID: verificationInsert.ReceiverID, - VerificationField: verificationInsert.VerificationField, - HashedValue: hashedVerification, - } + existingReceivers, err := di.receiverModel.GetByContacts(ctx, dbTx, contacts...) + if err != nil { + return nil, fmt.Errorf("fetching receivers by contacts: %w", err) + } - } else { - if verified := CompareVerificationValue(verification.HashedValue, instruction.disbursementInstruction.VerificationValue); !verified { - if verification.ConfirmedAt != nil { - return fmt.Errorf("%w: receiver verification for %s doesn't match. Check line %d on CSV file - Internal ID %s", ErrReceiverVerificationMismatch, receiver.PhoneNumber, instruction.line, instruction.disbursementInstruction.ID) - } - err = di.receiverVerificationModel.UpdateVerificationValue(ctx, dbTx, verification.ReceiverID, verification.VerificationField, instruction.disbursementInstruction.VerificationValue) - if err != nil { - return fmt.Errorf("error updating receiver verification for disbursement id %s: %w", disbursement.ID, err) - } - } - } + // Step 2: Create maps for quick lookup + existingReceiversByContactMap := make(map[string]*Receiver) + for _, receiver := range existingReceivers { + contact := receiver.ContactByType(contactType) + if contact == "" { + return nil, fmt.Errorf("receiver with ID %s has no contact information for contact type %s", receiver.ID, contactType) } + existingReceiversByContactMap[contact] = receiver + } - // Step 3: Fetch all receiver wallets and create missing ones - receiverWallets, err := di.receiverWalletModel.GetByReceiverIDsAndWalletID(ctx, dbTx, receiverIDs, disbursement.Wallet.ID) - if err != nil { - return fmt.Errorf("error fetching receiver wallets: %w", err) + // Step 3: Create missing receivers from instructions + for _, instruction := range instructions { + if createErr := di.createReceiverFromInstructionIfNeeded(ctx, dbTx, instruction, existingReceiversByContactMap); createErr != nil { + return nil, fmt.Errorf("creating receiver from instruction: %w", createErr) + } + } + + // Step 4: Fetch all receivers again + receivers, err := di.receiverModel.GetByContacts(ctx, dbTx, contacts...) + if err != nil { + return nil, fmt.Errorf("fetching receivers by contact information: %w", err) + } + + if len(receivers) != len(instructions) { + return nil, fmt.Errorf("receiver count mismatch after processing instructions") + } + + receiversByIDMap := make(map[string]*Receiver) + for _, receiver := range receivers { + receiversByIDMap[receiver.ID] = receiver + } + + return receiversByIDMap, nil +} + +// createReceiverFromInstructionIfNeeded create a new receiver if it doesn't exist for the given instruction. +func (di DisbursementInstructionModel) createReceiverFromInstructionIfNeeded(ctx context.Context, dbTx db.DBTransaction, instruction *DisbursementInstruction, existingReceiversByContactMap map[string]*Receiver) error { + contact, err := instruction.Contact() + if err != nil { + return fmt.Errorf("resolving contact information for instruction with ID %s: %w", instruction.ID, err) + } + + _, exists := existingReceiversByContactMap[contact] + if !exists { + var receiverInsert ReceiverInsert + if instruction.Phone != "" { + receiverInsert.PhoneNumber = &instruction.Phone } - receiverIDToReceiverWalletIDMap := make(map[string]string) - for _, receiverWallet := range receiverWallets { - receiverIDToReceiverWalletIDMap[receiverWallet.Receiver.ID] = receiverWallet.ID + if instruction.Email != "" { + receiverInsert.Email = &instruction.Email } + if instruction.ID != "" { + receiverInsert.ExternalId = &instruction.ID + } + _, insertErr := di.receiverModel.Insert(ctx, dbTx, receiverInsert) + if insertErr != nil { + return fmt.Errorf("inserting receiver: %w", insertErr) + } + } - for _, receiverId := range receiverIDs { - receiverWalletId, exists := receiverIDToReceiverWalletIDMap[receiverId] - if !exists { - receiverWalletInsert := ReceiverWalletInsert{ - ReceiverID: receiverId, - WalletID: disbursement.Wallet.ID, - } - rwID, insertErr := di.receiverWalletModel.Insert(ctx, dbTx, receiverWalletInsert) - if insertErr != nil { - return fmt.Errorf("error inserting receiver wallet for receiver id %s: %w", receiverId, insertErr) - } - receiverIDToReceiverWalletIDMap[receiverId] = rwID - } else { - _, retryErr := di.receiverWalletModel.RetryInvitationSMS(ctx, dbTx, receiverWalletId) - if retryErr != nil { - if !errors.Is(retryErr, ErrRecordNotFound) { - return fmt.Errorf("error retrying invitation: %w", retryErr) - } - } - } + return nil +} + +func (di DisbursementInstructionModel) processReceiverVerifications(ctx context.Context, dbTx db.DBTransaction, receiversByIDMap map[string]*Receiver, instructions []*DisbursementInstruction, disbursement *Disbursement, contactType ReceiverContactType) error { + receiverIDs := maps.Keys(receiversByIDMap) + + verifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbTx, receiverIDs, disbursement.VerificationField) + if err != nil { + return fmt.Errorf("fetching receiver verifications: %w", err) + } + + verificationByReceiverIDMap := make(map[string]*ReceiverVerification) + for _, verification := range verifications { + verificationByReceiverIDMap[verification.ReceiverID] = verification + } + + instructionsByContactMap := make(map[string]*DisbursementInstruction) + for _, instruction := range instructions { + contact, err := instruction.Contact() + if err != nil { + return fmt.Errorf("resolving contact information for instruction with ID %s: %w", instruction.ID, err) } + instructionsByContactMap[contact] = instruction + } - // Step 4: Delete all payments tied to this disbursement for each receiver in one call - if err = di.paymentModel.DeleteAllForDisbursement(ctx, dbTx, disbursement.ID); err != nil { - return fmt.Errorf("error deleting payments: %w", err) + for _, receiver := range receiversByIDMap { + contact := receiver.ContactByType(contactType) + if contact == "" { + return fmt.Errorf("receiver with ID %s has no contact information for contact type %s", receiver.ID, contactType) + } + instruction := instructionsByContactMap[contact] + if instruction == nil { + return fmt.Errorf("instruction not found for receiver with ID %s", receiver.ID) } + verification, exists := verificationByReceiverIDMap[receiver.ID] - // Step 5: Create payments for all receivers - payments := make([]PaymentInsert, 0, len(instructions)) - for _, instruction := range instructions { - receiver := receiverMap[instruction.Phone] - payment := PaymentInsert{ + if !exists { + verificationInsert := ReceiverVerificationInsert{ ReceiverID: receiver.ID, - DisbursementID: disbursement.ID, - Amount: instruction.Amount, - AssetID: disbursement.Asset.ID, - ReceiverWalletID: receiverIDToReceiverWalletIDMap[receiver.ID], - ExternalPaymentID: instruction.ExternalPaymentId, + VerificationValue: instruction.VerificationValue, + VerificationField: disbursement.VerificationField, + } + _, insertErr := di.receiverVerificationModel.Insert(ctx, dbTx, verificationInsert) + if insertErr != nil { + return fmt.Errorf("error inserting receiver verification: %w", insertErr) + } + } else if !CompareVerificationValue(verification.HashedValue, instruction.VerificationValue) { + if verification.ConfirmedAt != nil { + return fmt.Errorf("%w: receiver verification for %s doesn't match. Check instruction with ID %s", ErrReceiverVerificationMismatch, contact, instruction.ID) + } + updateErr := di.receiverVerificationModel.UpdateVerificationValue(ctx, dbTx, verification.ReceiverID, verification.VerificationField, instruction.VerificationValue) + if updateErr != nil { + return fmt.Errorf("error updating receiver verification for disbursement id %s: %w", disbursement.ID, updateErr) } - payments = append(payments, payment) - } - if err = di.paymentModel.InsertAll(ctx, dbTx, payments); err != nil { - return fmt.Errorf("error inserting payments: %w", err) } + } - // Step 6: Persist Payment file to Disbursement - if err = di.disbursementModel.Update(ctx, update); err != nil { - return fmt.Errorf("error persisting payment file: %w", err) + return nil +} + +func (di DisbursementInstructionModel) processReceiverWallets(ctx context.Context, dbTx db.DBTransaction, receiversByIDMap map[string]*Receiver, disbursement *Disbursement) (map[string]string, error) { + receiverIDs := maps.Keys(receiversByIDMap) + + receiverWallets, err := di.receiverWalletModel.GetByReceiverIDsAndWalletID(ctx, dbTx, receiverIDs, disbursement.Wallet.ID) + if err != nil { + return nil, fmt.Errorf("fetching receiver wallets: %w", err) + } + receiverIDToReceiverWalletIDMap := make(map[string]string) + for _, receiverWallet := range receiverWallets { + receiverIDToReceiverWalletIDMap[receiverWallet.Receiver.ID] = receiverWallet.ID + } + + for receiverID := range receiversByIDMap { + receiverWalletID, exists := receiverIDToReceiverWalletIDMap[receiverID] + if !exists { + receiverWalletInsert := ReceiverWalletInsert{ + ReceiverID: receiverID, + WalletID: disbursement.Wallet.ID, + } + rwID, insertErr := di.receiverWalletModel.Insert(ctx, dbTx, receiverWalletInsert) + if insertErr != nil { + return nil, fmt.Errorf("inserting receiver wallet for receiver id %s: %w", receiverID, insertErr) + } + receiverIDToReceiverWalletIDMap[receiverID] = rwID + } else { + _, retryErr := di.receiverWalletModel.RetryInvitationSMS(ctx, dbTx, receiverWalletID) + if retryErr != nil { + if !errors.Is(retryErr, ErrRecordNotFound) { + return nil, fmt.Errorf("retrying invitation: %w", retryErr) + } + } } + } - // Step 7: Update Disbursement Status - if err = di.disbursementModel.UpdateStatus(ctx, dbTx, userID, disbursement.ID, ReadyDisbursementStatus); err != nil { - return fmt.Errorf("error updating status: %w", err) + return receiverIDToReceiverWalletIDMap, nil +} + +func (di DisbursementInstructionModel) createPayments(ctx context.Context, dbTx db.DBTransaction, receiverMap map[string]*Receiver, receiverIDToReceiverWalletIDMap map[string]string, instructions []*DisbursementInstruction, disbursement *Disbursement) error { + payments := make([]PaymentInsert, 0, len(instructions)) + + for _, instruction := range instructions { + receiver := findReceiverByInstruction(receiverMap, instruction) + if receiver == nil { + return fmt.Errorf("receiver not found for instruction with ID %s", instruction.ID) } + payment := PaymentInsert{ + ReceiverID: receiver.ID, + DisbursementID: disbursement.ID, + Amount: instruction.Amount, + AssetID: disbursement.Asset.ID, + ReceiverWalletID: receiverIDToReceiverWalletIDMap[receiver.ID], + } + if instruction.ExternalPaymentId != "" { + payment.ExternalPaymentID = &instruction.ExternalPaymentId + } + payments = append(payments, payment) + } + + if err := di.paymentModel.InsertAll(ctx, dbTx, payments); err != nil { + return fmt.Errorf("inserting payments: %w", err) + } + + return nil +} +func findReceiverByInstruction(receiverMap map[string]*Receiver, instruction *DisbursementInstruction) *Receiver { + contact, err := instruction.Contact() + if err != nil { return nil - }) + } + + for _, receiver := range receiverMap { + if contact == receiver.PhoneNumber || contact == receiver.Email { + return receiver + } + } + return nil } diff --git a/internal/data/disbursement_instructions_test.go b/internal/data/disbursement_instructions_test.go index 4d7c3398d..5379cfe61 100644 --- a/internal/data/disbursement_instructions_test.go +++ b/internal/data/disbursement_instructions_test.go @@ -3,6 +3,7 @@ package data import ( "context" "database/sql" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -35,53 +36,92 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { di := NewDisbursementInstructionModel(dbConnectionPool) - instruction1 := DisbursementInstruction{ + smsInstruction1 := DisbursementInstruction{ Phone: "+380-12-345-671", Amount: "100.01", ID: "123456781", VerificationValue: "1990-01-01", } - instruction2 := DisbursementInstruction{ + smsInstruction2 := DisbursementInstruction{ Phone: "+380-12-345-672", Amount: "100.02", ID: "123456782", VerificationValue: "1990-01-02", } - externalPaymentID := "abc123" - instruction3 := DisbursementInstruction{ + smsInstruction3 := DisbursementInstruction{ Phone: "+380-12-345-673", Amount: "100.03", ID: "123456783", VerificationValue: "1990-01-03", - ExternalPaymentId: &externalPaymentID, + ExternalPaymentId: "abc123", } - instructions := []*DisbursementInstruction{&instruction1, &instruction2, &instruction3} - expectedPhoneNumbers := []string{instruction1.Phone, instruction2.Phone, instruction3.Phone} - expectedExternalIDs := []string{instruction1.ID, instruction2.ID, instruction3.ID} - expectedPayments := []string{instruction1.Amount, instruction2.Amount, instruction3.Amount} - expectedExternalPaymentIDs := []string{*instruction3.ExternalPaymentId} + + emailInstruction1 := DisbursementInstruction{ + Email: "receiver1@stellar.org", + Amount: "100.01", + ID: "123456781", + VerificationValue: "1990-01-01", + } + + emailInstruction2 := DisbursementInstruction{ + Email: "receiver2@stellar.org", + Amount: "100.02", + ID: "123456782", + VerificationValue: "1990-01-02", + } + + emailInstruction3 := DisbursementInstruction{ + Email: "receiver3@stellar.org", + Amount: "100.03", + ID: "123456783", + VerificationValue: "1990-01-03", + } + + smsInstructions := []*DisbursementInstruction{&smsInstruction1, &smsInstruction2, &smsInstruction3} + emailInstructions := []*DisbursementInstruction{&emailInstruction1, &emailInstruction2, &emailInstruction3} + expectedPhoneNumbers := []string{smsInstruction1.Phone, smsInstruction2.Phone, smsInstruction3.Phone} + expectedEmails := []string{emailInstruction1.Email, emailInstruction2.Email, emailInstruction3.Email} + expectedExternalIDs := []string{smsInstruction1.ID, smsInstruction2.ID, smsInstruction3.ID} + expectedPayments := []string{smsInstruction1.Amount, smsInstruction2.Amount, smsInstruction3.Amount} + expectedExternalPaymentIDs := []string{smsInstruction3.ExternalPaymentId} disbursementUpdate := &DisbursementUpdate{ ID: disbursement.ID, FileName: "instructions.csv", - FileContent: CreateInstructionsFixture(t, instructions), + FileContent: CreateInstructionsFixture(t, smsInstructions), + } + + cleanup := func() { + DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) + DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool) + DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) + DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) } - t.Run("success", func(t *testing.T) { - err := di.ProcessAll(ctx, "user-id", instructions, disbursement, disbursementUpdate, MaxInstructionsPerDisbursement) + t.Run("success - sms instructions", func(t *testing.T) { + defer cleanup() + + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: smsInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + ReceiverContactType: ReceiverContactTypeSMS, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) require.NoError(t, err) // Verify Receivers - receivers, err := di.receiverModel.GetByPhoneNumbers(ctx, dbConnectionPool, []string{instruction1.Phone, instruction2.Phone, instruction3.Phone}) + receivers, err := di.receiverModel.GetByContacts(ctx, dbConnectionPool, smsInstruction1.Phone, smsInstruction2.Phone, smsInstruction3.Phone) require.NoError(t, err) assertEqualReceivers(t, expectedPhoneNumbers, expectedExternalIDs, receivers) // Verify ReceiverVerifications receiverVerifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID, receivers[2].ID}, VerificationTypeDateOfBirth) require.NoError(t, err) - assertEqualVerifications(t, instructions, receiverVerifications, receivers) + assertEqualVerifications(t, smsInstructions, receiverVerifications, receivers) // Verify ReceiverWallets receiverWallets, err := di.receiverWalletModel.GetByReceiverIDsAndWalletID(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID, receivers[2].ID}, wallet.ID) @@ -108,29 +148,103 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { require.Equal(t, disbursementUpdate.FileName, actualDisbursement.FileName) }) + t.Run("success - email instructions", func(t *testing.T) { + defer cleanup() + + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: emailInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + ReceiverContactType: ReceiverContactTypeEmail, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) + require.NoError(t, err) + + // Verify Receivers + receivers, err := di.receiverModel.GetByContacts(ctx, dbConnectionPool, emailInstruction1.Email, emailInstruction2.Email, emailInstruction3.Email) + require.NoError(t, err) + assert.Len(t, receivers, len(expectedEmails)) + for _, actual := range receivers { + assert.Empty(t, actual.PhoneNumber) + assert.Contains(t, expectedEmails, actual.Email) + assert.Contains(t, expectedExternalIDs, actual.ExternalID) + } + }) + + t.Run("failure - email instructions without email fields", func(t *testing.T) { + defer cleanup() + + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: smsInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + ReceiverContactType: ReceiverContactTypeEmail, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) + require.ErrorContains(t, err, "has no contact information for contact type EMAIL") + }) + + t.Run("failure - email instructions with email and phone fields", func(t *testing.T) { + defer cleanup() + + emailAndSMSInstructions := []*DisbursementInstruction{ + { + Phone: "+380-12-345-671", + Email: "receiver1@stellar.org", + Amount: "100.01", + ID: "123456781", + VerificationValue: "1990-01-01", + }, + } + + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + 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" + assert.ErrorContains(t, err, fmt.Sprintf(errorMsg, emailAndSMSInstructions[0].ID)) + }) + t.Run("success - Not confirmed Verification Value updated", func(t *testing.T) { - DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) - DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool) - DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) - DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) + defer cleanup() // process instructions for the first time - err := di.ProcessAll(ctx, "user-id", instructions, disbursement, disbursementUpdate, MaxInstructionsPerDisbursement) + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: smsInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + ReceiverContactType: ReceiverContactTypeSMS, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) require.NoError(t, err) - instruction1.VerificationValue = "1990-01-04" - err = di.ProcessAll(ctx, "user-id", instructions, disbursement, disbursementUpdate, MaxInstructionsPerDisbursement) + smsInstruction1.VerificationValue = "1990-01-04" + err = di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: smsInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + ReceiverContactType: ReceiverContactTypeSMS, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) require.NoError(t, err) // Verify Receivers - receivers, err := di.receiverModel.GetByPhoneNumbers(ctx, dbConnectionPool, []string{instruction1.Phone, instruction2.Phone, instruction3.Phone}) + receivers, err := di.receiverModel.GetByContacts(ctx, dbConnectionPool, smsInstruction1.Phone, smsInstruction2.Phone, smsInstruction3.Phone) require.NoError(t, err) assertEqualReceivers(t, expectedPhoneNumbers, expectedExternalIDs, receivers) // Verify ReceiverVerifications receiverVerifications, err := di.receiverVerificationModel.GetByReceiverIDsAndVerificationField(ctx, dbConnectionPool, []string{receivers[0].ID, receivers[1].ID, receivers[2].ID}, VerificationTypeDateOfBirth) require.NoError(t, err) - assertEqualVerifications(t, instructions, receiverVerifications, receivers) + assertEqualVerifications(t, smsInstructions, receiverVerifications, receivers) // Verify Disbursement actualDisbursement, err := di.disbursementModel.Get(ctx, dbConnectionPool, disbursement.ID) @@ -141,6 +255,8 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { }) t.Run("success - existing receiver wallet", func(t *testing.T) { + defer cleanup() + // New instructions readyDisbursement := CreateDisbursementFixture(t, ctx, dbConnectionPool, &DisbursementModel{dbConnectionPool: dbConnectionPool}, &Disbursement{ Name: "readyDisbursement", @@ -180,10 +296,17 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { FileContent: CreateInstructionsFixture(t, newInstructions), } - err := di.ProcessAll(ctx, "user-id", newInstructions, readyDisbursement, readyDisbursementUpdate, MaxInstructionsPerDisbursement) + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: newInstructions, + Disbursement: readyDisbursement, + DisbursementUpdate: readyDisbursementUpdate, + ReceiverContactType: ReceiverContactTypeSMS, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) require.NoError(t, err) - receivers, err := di.receiverModel.GetByPhoneNumbers(ctx, dbConnectionPool, []string{newInstruction1.Phone, newInstruction2.Phone, newInstruction3.Phone}) + receivers, err := di.receiverModel.GetByContacts(ctx, dbConnectionPool, newInstruction1.Phone, newInstruction2.Phone, newInstruction3.Phone) require.NoError(t, err) assertEqualReceivers(t, newExpectedPhoneNumbers, newExpectedExternalIDs, receivers) @@ -217,7 +340,14 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { assert.NotNil(t, receiverWallet.InvitationSentAt) } - err = di.ProcessAll(ctx, "user-id", newInstructions, readyDisbursement, readyDisbursementUpdate, MaxInstructionsPerDisbursement) + err = di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: newInstructions, + Disbursement: readyDisbursement, + DisbursementUpdate: readyDisbursementUpdate, + ReceiverContactType: ReceiverContactTypeSMS, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) require.NoError(t, err) // Verify ReceiverWallets @@ -237,22 +367,25 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { }) t.Run("failure - Too many instructions", func(t *testing.T) { - err := di.ProcessAll(ctx, "user-id", instructions, disbursement, disbursementUpdate, 2) + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: smsInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + MaxNumberOfInstructions: 2, + }) require.EqualError(t, err, "maximum number of instructions exceeded") }) t.Run("failure - Confirmed Verification Value not matching", func(t *testing.T) { - DeleteAllPaymentsFixtures(t, ctx, dbConnectionPool) - DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool) - DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) - DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) + defer cleanup() instruction4 := DisbursementInstruction{ Phone: "+380-12-345-674", Amount: "100.04", ID: "123456784", VerificationValue: "1990-01-04", - ExternalPaymentId: &externalPaymentID, + ExternalPaymentId: "abc123", } instruction5 := DisbursementInstruction{ @@ -260,7 +393,7 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { Amount: "100.05", ID: "123456785", VerificationValue: "1990-01-05", - ExternalPaymentId: &externalPaymentID, + ExternalPaymentId: "abc123", } instruction6 := DisbursementInstruction{ @@ -268,14 +401,21 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { Amount: "100.06", ID: "123456786", VerificationValue: "1990-01-06", - ExternalPaymentId: &externalPaymentID, + ExternalPaymentId: "abc123", } // process instructions for the first time - err := di.ProcessAll(ctx, "user-id", instructions, disbursement, disbursementUpdate, MaxInstructionsPerDisbursement) + err := di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: smsInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + ReceiverContactType: ReceiverContactTypeSMS, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) require.NoError(t, err) - receivers, err := di.receiverModel.GetByPhoneNumbers(ctx, dbConnectionPool, []string{instruction1.Phone, instruction2.Phone, instruction3.Phone, instruction4.Phone, instruction5.Phone, instruction6.Phone}) + receivers, err := di.receiverModel.GetByContacts(ctx, dbConnectionPool, smsInstruction1.Phone, smsInstruction2.Phone, smsInstruction3.Phone, instruction4.Phone, instruction5.Phone, instruction6.Phone) require.NoError(t, err) receiversMap := make(map[string]*Receiver) for _, receiver := range receivers { @@ -283,13 +423,20 @@ func Test_DisbursementInstructionModel_ProcessAll(t *testing.T) { } // confirm a verification - ConfirmVerificationForRecipient(t, ctx, dbConnectionPool, receiversMap[instruction3.Phone].ID) + ConfirmVerificationForRecipient(t, ctx, dbConnectionPool, receiversMap[smsInstruction3.Phone].ID) // process instructions with mismatched verification values - instruction3.VerificationValue = "1990-01-07" - err = di.ProcessAll(ctx, "user-id", instructions, disbursement, disbursementUpdate, MaxInstructionsPerDisbursement) + smsInstruction3.VerificationValue = "1990-01-07" + err = di.ProcessAll(ctx, DisbursementInstructionsOpts{ + UserID: "user-id", + Instructions: smsInstructions, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + ReceiverContactType: ReceiverContactTypeSMS, + MaxNumberOfInstructions: MaxInstructionsPerDisbursement, + }) require.Error(t, err) - assert.EqualError(t, err, "running atomic function in RunInTransactionWithResult: receiver verification mismatch: receiver verification for +380-12-345-673 doesn't match. Check line 3 on CSV file - Internal ID 123456783") + assert.EqualError(t, err, "running atomic function in RunInTransactionWithResult: processing receiver verifications: receiver verification mismatch: receiver verification for +380-12-345-673 doesn't match. Check instruction with ID 123456783") }) } @@ -297,6 +444,7 @@ func assertEqualReceivers(t *testing.T, expectedPhones, expectedExternalIDs []st assert.Len(t, actualReceivers, len(expectedPhones)) for _, actual := range actualReceivers { + assert.NotNil(t, actual.PhoneNumber) assert.Contains(t, expectedPhones, actual.PhoneNumber) assert.Contains(t, expectedExternalIDs, actual.ExternalID) } diff --git a/internal/data/disbursement_receivers.go b/internal/data/disbursement_receivers.go index b6cfa6353..9f4738968 100644 --- a/internal/data/disbursement_receivers.go +++ b/internal/data/disbursement_receivers.go @@ -46,8 +46,8 @@ func (m DisbursementReceiverModel) GetAll(ctx context.Context, sqlExec db.SQLExe baseQuery := ` SELECT r.id, - r.phone_number, r.external_id, + COALESCE(r.phone_number, '') as phone_number, COALESCE(r.email, '') as email, r.created_at, r.updated_at, diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go index cd5cf313d..222f2a437 100644 --- a/internal/data/disbursements_test.go +++ b/internal/data/disbursements_test.go @@ -442,9 +442,9 @@ func Test_DisbursementModel_Update(t *testing.T) { }) disbursementFileContent := CreateInstructionsFixture(t, []*DisbursementInstruction{ - {"1234567890", "1", "123.12", "1995-02-20", nil}, - {"0987654321", "2", "321", "1974-07-19", nil}, - {"0987654321", "3", "321", "1974-07-19", nil}, + {"1234567890", "", "1", "123.12", "1995-02-20", ""}, + {"0987654321", "", "2", "321", "1974-07-19", ""}, + {"0987654321", "", "3", "321", "1974-07-19", ""}, }) t.Run("update instructions", func(t *testing.T) { diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go index 5df2bc928..6f5edb802 100644 --- a/internal/data/fixtures.go +++ b/internal/data/fixtures.go @@ -290,9 +290,8 @@ func CreateReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExec randomSuffix, err := utils.RandomString(5) require.NoError(t, err) - if r.Email == nil { - email := fmt.Sprintf("email%s@randomemail.com", randomSuffix) - r.Email = &email + if r.Email == "" { + r.Email = fmt.Sprintf("email%s@randomemail.com", randomSuffix) } if r.PhoneNumber == "" { diff --git a/internal/data/fixtures_test.go b/internal/data/fixtures_test.go index f04913adb..7d26ea3a9 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", nil}, - {"0987654321", "2", "321", "1974-07-19", nil}, + {"1234567890", "", "1", "123.12", "1995-02-20", ""}, + {"0987654321", "", "2", "321", "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", nil}, - {"0987654321", "2", "321", "1974-07-19", nil}, - {"0987654321", "3", "321", "1974-07-19", nil}, + {"1234567890", "", "1", "123.12", "1995-02-20", ""}, + {"0987654321", "", "2", "321", "1974-07-19", ""}, + {"0987654321", "", "3", "321", "1974-07-19", ""}, } t.Run("update instructions", func(t *testing.T) { diff --git a/internal/data/receivers.go b/internal/data/receivers.go index 16fc3545f..5f66e8d66 100644 --- a/internal/data/receivers.go +++ b/internal/data/receivers.go @@ -17,14 +17,32 @@ import ( type Receiver struct { ID string `json:"id" db:"id"` - Email *string `json:"email,omitempty" db:"email"` - PhoneNumber string `json:"phone_number,omitempty" db:"phone_number"` ExternalID string `json:"external_id,omitempty" db:"external_id"` CreatedAt *time.Time `json:"created_at,omitempty" db:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty" db:"updated_at"` + Email string `json:"email,omitempty" db:"email"` + PhoneNumber string `json:"phone_number,omitempty" db:"phone_number"` ReceiverStats } +type ReceiverContactType string + +const ( + ReceiverContactTypeEmail ReceiverContactType = "EMAIL" + ReceiverContactTypeSMS ReceiverContactType = "PHONE_NUMBER" +) + +func (r Receiver) ContactByType(contactType ReceiverContactType) string { + switch contactType { + case ReceiverContactTypeEmail: + return r.Email + case ReceiverContactTypeSMS: + return r.PhoneNumber + default: + return "" + } +} + type ReceiverRegistrationRequest struct { // TODO: SDP-1296 - Update `/wallet-registration/otp` to support multiple contact information types and send OTPs accordingly Email string `json:"email"` @@ -61,13 +79,35 @@ var ( type ReceiverModel struct{} type ReceiverInsert struct { - PhoneNumber string `db:"phone_number"` + PhoneNumber *string `db:"phone_number"` + Email *string `db:"email"` ExternalId *string `db:"external_id"` } -type ReceiverUpdate struct { - Email string `db:"email"` - ExternalId string `db:"external_id"` +type ReceiverUpdate ReceiverInsert + +func (ru ReceiverUpdate) IsEmpty() bool { + return ru.Email == nil && ru.ExternalId == nil && ru.PhoneNumber == nil +} + +func (ru ReceiverUpdate) Validate() error { + if ru.IsEmpty() { + return fmt.Errorf("no values provided to update receiver") + } + + if ru.Email != nil { + if err := utils.ValidateEmail(*ru.Email); err != nil { + return fmt.Errorf("validating email: %w", err) + } + } + + if ru.PhoneNumber != nil { + if err := utils.ValidatePhoneNumber(*ru.PhoneNumber); err != nil { + return fmt.Errorf("validating phone number: %w", err) + } + } + + return nil } type ReceivedAmounts []Amount @@ -145,8 +185,8 @@ func (r *ReceiverModel) Get(ctx context.Context, sqlExec db.SQLExecuter, id stri SELECT rc.id, rc.external_id, + COALESCE(rc.phone_number, '') as phone_number, COALESCE(rc.email, '') as email, - rc.phone_number, rc.created_at, rc.updated_at, COALESCE(total_payments, 0) as total_payments, @@ -238,7 +278,7 @@ func (r *ReceiverModel) GetAll(ctx context.Context, sqlExec db.SQLExecuter, quer distinct(r.id), r.external_id, COALESCE(r.email, '') as email, - r.phone_number, + COALESCE(r.phone_number, '') as phone_number, r.created_at, r.updated_at, COALESCE(total_payments, 0) as total_payments, @@ -253,13 +293,12 @@ func (r *ReceiverModel) GetAll(ctx context.Context, sqlExec db.SQLExecuter, quer LEFT JOIN receiver_wallets rw ON rw.receiver_id = r.id LEFT JOIN registered_receiver_wallets_count_cte rrwc ON rrwc.receiver_id = r.id ` - receiverQuery := ` SELECT r.id, - r.email, + COALESCE(r.phone_number, '') as phone_number, + COALESCE(r.email, '') as email, r.external_id, - r.phone_number, r.created_at, r.updated_at FROM @@ -320,22 +359,25 @@ func (r *ReceiverModel) Insert(ctx context.Context, sqlExec db.SQLExecuter, inse query := ` INSERT INTO receivers ( phone_number, + email, external_id ) VALUES ( $1, - $2 + $2, + $3 ) RETURNING id, - phone_number, + COALESCE(phone_number, '') as phone_number, + COALESCE(email, '') as email, external_id, created_at, updated_at ` var receiver Receiver - err := sqlExec.GetContext(ctx, &receiver, query, insert.PhoneNumber, insert.ExternalId) + err := sqlExec.GetContext(ctx, &receiver, query, insert.PhoneNumber, insert.Email, insert.ExternalId) if err != nil { - return nil, fmt.Errorf("error inserting receiver: %w", err) + return nil, fmt.Errorf("inserting receiver: %w", err) } return &receiver, nil @@ -343,24 +385,29 @@ func (r *ReceiverModel) Insert(ctx context.Context, sqlExec db.SQLExecuter, inse // Update updates the receiver Email and/or External ID. func (r *ReceiverModel) Update(ctx context.Context, sqlExec db.SQLExecuter, ID string, receiverUpdate ReceiverUpdate) error { - if receiverUpdate.Email == "" && receiverUpdate.ExternalId == "" { - return fmt.Errorf("provide at least one of these values: Email or ExternalID") + if err := receiverUpdate.Validate(); err != nil { + return fmt.Errorf("validating receiver update: %w", err) } args := []interface{}{} fields := []string{} - if receiverUpdate.Email != "" { - if err := utils.ValidateEmail(receiverUpdate.Email); err != nil { - return fmt.Errorf("error validating email: %w", err) - } + if receiverUpdate.PhoneNumber != nil { + phoneNumber := *receiverUpdate.PhoneNumber + fields = append(fields, "phone_number = ?") + args = append(args, phoneNumber) + } + + if receiverUpdate.Email != nil { + email := *receiverUpdate.Email fields = append(fields, "email = ?") - args = append(args, receiverUpdate.Email) + args = append(args, email) } - if receiverUpdate.ExternalId != "" { + if receiverUpdate.ExternalId != nil { + externalID := *receiverUpdate.ExternalId fields = append(fields, "external_id = ?") - args = append(args, receiverUpdate.ExternalId) + args = append(args, externalID) } args = append(args, ID) @@ -378,29 +425,34 @@ func (r *ReceiverModel) Update(ctx context.Context, sqlExec db.SQLExecuter, ID s _, err := sqlExec.ExecContext(ctx, query, args...) if err != nil { - return fmt.Errorf("error updating receiver: %w", err) + return fmt.Errorf("updating receiver: %w", err) } return nil } -// GetByPhoneNumbers search for receivers by phone numbers -func (r *ReceiverModel) GetByPhoneNumbers(ctx context.Context, sqlExec db.SQLExecuter, ids []string) ([]*Receiver, error) { +// GetByContacts search for receivers by phone numbers and email. +func (r *ReceiverModel) GetByContacts(ctx context.Context, sqlExec db.SQLExecuter, contacts ...string) ([]*Receiver, error) { receivers := []*Receiver{} + if len(contacts) == 0 { + return receivers, nil + } + query := ` SELECT r.id, - r.phone_number, + COALESCE(r.phone_number, '') as phone_number, + COALESCE(r.email, '') as email, r.external_id, r.created_at, r.updated_at FROM receivers r - WHERE r.phone_number = ANY($1) + WHERE r.phone_number = ANY($1) OR r.email = ANY($1) ` - err := sqlExec.SelectContext(ctx, &receivers, query, pq.Array(ids)) + err := sqlExec.SelectContext(ctx, &receivers, query, pq.Array(contacts)) if err != nil { - return nil, fmt.Errorf("error fetching receiver ids by phone numbers: %w", err) + return nil, fmt.Errorf("fetching receivers by phone numbers or email: %w", err) } return receivers, nil } diff --git a/internal/data/receivers_test.go b/internal/data/receivers_test.go index 8878356d3..d391173bf 100644 --- a/internal/data/receivers_test.go +++ b/internal/data/receivers_test.go @@ -17,7 +17,7 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" ) -func Test_ReceiversModelGet(t *testing.T) { +func Test_ReceiversModel_Get(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() @@ -338,7 +338,7 @@ func Test_ReceiversModelGet(t *testing.T) { }) } -func Test_ReceiversModelCount(t *testing.T) { +func Test_ReceiversModel_Count(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() @@ -428,7 +428,7 @@ func Test_ReceiversModelCount(t *testing.T) { }) } -func Test_ReceiversModelGetAll(t *testing.T) { +func Test_ReceiversModel_GetAll(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() @@ -462,7 +462,7 @@ func Test_ReceiversModelGetAll(t *testing.T) { date := time.Date(2023, 1, 10, 23, 40, 20, 1431, time.UTC) receiver1Email := "receiver1@mock.com" receiver1 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{ - Email: &receiver1Email, + Email: receiver1Email, PhoneNumber: "+99991111", ExternalID: "external-id-1", CreatedAt: &date, @@ -472,7 +472,7 @@ func Test_ReceiversModelGetAll(t *testing.T) { date = time.Date(2023, 3, 10, 23, 40, 20, 1431, time.UTC) receiver2Email := "receiver2@mock.com" receiver2 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{ - Email: &receiver2Email, + Email: receiver2Email, PhoneNumber: "+99992222", ExternalID: "external-id-2", CreatedAt: &date, @@ -853,9 +853,8 @@ func Test_ReceiversModel_GetAll_makeSureReceiversWithMultipleWalletsWillReturnAS wallet1 := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet1", "https://www.wallet.com", "www.wallet.com", "wallet1://") wallet2 := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet2", "https://www.wallet2.com", "www.wallet2.com", "wallet2://") - receiver1Email := "receiver1@mock.com" receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{ - Email: &receiver1Email, + Email: "receiver1@mock.com", PhoneNumber: "+99991111", ExternalID: "external-id-1", }) @@ -1110,7 +1109,7 @@ func Test_ReceiversModel_Update(t *testing.T) { receiverModel := ReceiverModel{} email, externalID := "receiver@email.com", "externalID" - receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{Email: &email, ExternalID: externalID}) + receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{Email: email, ExternalID: externalID}) resetReceiver := func(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, receiverID string) { q := ` @@ -1123,21 +1122,18 @@ func Test_ReceiversModel_Update(t *testing.T) { t.Run("returns error when no value is provided", func(t *testing.T) { resetReceiver(t, ctx, dbConnectionPool, receiver.ID) - err = receiverModel.Update(ctx, dbConnectionPool, receiver.ID, ReceiverUpdate{ - Email: "", - ExternalId: "", - }) - assert.EqualError(t, err, "provide at least one of these values: Email or ExternalID") + err = receiverModel.Update(ctx, dbConnectionPool, receiver.ID, ReceiverUpdate{}) + assert.EqualError(t, err, "validating receiver update: no values provided to update receiver") }) t.Run("returns error when email is invalid", func(t *testing.T) { resetReceiver(t, ctx, dbConnectionPool, receiver.ID) err = receiverModel.Update(ctx, dbConnectionPool, receiver.ID, ReceiverUpdate{ - Email: "invalid", - ExternalId: "", + Email: utils.StringPtr("invalid"), + ExternalId: utils.StringPtr(""), }) - assert.EqualError(t, err, `error validating email: the provided email is not valid`) + assert.EqualError(t, err, `validating receiver update: validating email: the provided email is not valid`) }) t.Run("updates email name successfully", func(t *testing.T) { @@ -1145,19 +1141,18 @@ func Test_ReceiversModel_Update(t *testing.T) { receiver, err = receiverModel.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, email, *receiver.Email) + assert.Equal(t, email, receiver.Email) assert.Equal(t, externalID, receiver.ExternalID) err = receiverModel.Update(ctx, dbConnectionPool, receiver.ID, ReceiverUpdate{ - Email: "updated_email@email.com", - ExternalId: "", + Email: utils.StringPtr("updated_email@email.com"), }) require.NoError(t, err) receiver, err = receiverModel.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.NotEqual(t, email, *receiver.Email) - assert.Equal(t, "updated_email@email.com", *receiver.Email) + assert.NotEqual(t, email, receiver.Email) + assert.Equal(t, "updated_email@email.com", receiver.Email) assert.Equal(t, externalID, receiver.ExternalID) }) @@ -1166,20 +1161,90 @@ func Test_ReceiversModel_Update(t *testing.T) { receiver, err = receiverModel.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, email, *receiver.Email) + assert.Equal(t, email, receiver.Email) assert.Equal(t, externalID, receiver.ExternalID) err := receiverModel.Update(ctx, dbConnectionPool, receiver.ID, ReceiverUpdate{ - Email: "updated_email@email.com", - ExternalId: "newExternalID", + Email: utils.StringPtr("updated_email@email.com"), + ExternalId: utils.StringPtr("newExternalID"), }) require.NoError(t, err) receiver, err = receiverModel.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.NotEqual(t, email, *receiver.Email) - assert.Equal(t, "updated_email@email.com", *receiver.Email) + assert.NotEqual(t, email, receiver.Email) + assert.Equal(t, "updated_email@email.com", receiver.Email) assert.NotEqual(t, externalID, receiver.ExternalID) assert.Equal(t, "newExternalID", receiver.ExternalID) }) } + +func Test_ReceiversModel_GetByContacts(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() + + receiverModel := ReceiverModel{} + + receiver1 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{ + Email: "receiver1@stellar.org", + PhoneNumber: "+99991111", + ExternalID: "external-id-1", + }) + + receiver2 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{ + Email: "receiver2@stellar.org", + ExternalID: "external-id-2", + }) + + receiver3 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{ + PhoneNumber: "+99992222", + ExternalID: "external-id-3", + }) + + testCases := []struct { + name string + contacts []string + want []*Receiver + wantErr error + }{ + { + name: "list of contacts is empty", + contacts: []string{}, + want: []*Receiver{}, + wantErr: nil, + }, + { + name: "successfully get receivers by email and phone", + contacts: []string{receiver1.PhoneNumber, receiver2.Email, receiver3.PhoneNumber}, + want: []*Receiver{receiver1, receiver2, receiver3}, + }, + { + name: "successfully get receivers by email", + contacts: []string{receiver1.Email, receiver2.Email}, + want: []*Receiver{receiver1, receiver2}, + }, + { + name: "successfully get receivers by phone", + contacts: []string{receiver1.PhoneNumber, receiver3.PhoneNumber}, + want: []*Receiver{receiver1, receiver3}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + receivers, err := receiverModel.GetByContacts(ctx, dbConnectionPool, tc.contacts...) + if tc.wantErr != nil { + assert.EqualError(t, err, tc.wantErr.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, tc.want, receivers) + } + }) + } +} diff --git a/internal/data/receivers_wallet.go b/internal/data/receivers_wallet.go index 555e699f2..947387ac3 100644 --- a/internal/data/receivers_wallet.go +++ b/internal/data/receivers_wallet.go @@ -229,8 +229,8 @@ const getPendingRegistrationReceiverWalletsBaseQuery = ` rw.id, rw.invitation_sent_at, r.id AS "receiver.id", - r.phone_number AS "receiver.phone_number", - r.email AS "receiver.email", + COALESCE(r.phone_number, '') as "receiver.phone_number", + COALESCE(r.email, '') as "receiver.email", w.id AS "wallet.id", w.name AS "wallet.name" FROM diff --git a/internal/integrationtests/utils_test.go b/internal/integrationtests/utils_test.go index 2c5c97a18..d7bdc9afd 100644 --- a/internal/integrationtests/utils_test.go +++ b/internal/integrationtests/utils_test.go @@ -54,10 +54,11 @@ func Test_readDisbursementCSV(t *testing.T) { t.Run("reading csv file", func(t *testing.T) { data, err := readDisbursementCSV("resources", "disbursement_integration_tests.csv") require.NoError(t, err) - assert.Equal(t, data[0].Amount, "0.1") - assert.Equal(t, data[0].Phone, "+12025550191") - assert.Equal(t, data[0].ID, "1") - assert.Equal(t, data[0].VerificationValue, "1999-03-30") + assert.Equal(t, "0.1", data[0].Amount) + assert.NotNil(t, data[0].Phone) + assert.Equal(t, "+12025550191", data[0].Phone) + assert.Equal(t, "1", data[0].ID) + assert.Equal(t, "1999-03-30", data[0].VerificationValue) }) } 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 408ee2704..611fb2649 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 @@ -236,12 +236,14 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { Twice(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, + ToEmail: receiver1.Email, Message: contentWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(mockErr). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, + ToEmail: receiver2.Email, Message: contentWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go index 250879d5f..0efaf102b 100644 --- a/internal/serve/httphandler/disbursement_handler.go +++ b/internal/serve/httphandler/disbursement_handler.go @@ -3,12 +3,14 @@ package httphandler import ( "bytes" "context" + "encoding/csv" "encoding/json" "errors" "fmt" "io" "net/http" "slices" + "strings" "time" "github.com/go-chi/chi/v5" @@ -217,12 +219,20 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter, } defer file.Close() - // TeeReader is used to read multiple times from the same reader (file) - // We read once to process the instructions, and then again to persist the file to the database var buf bytes.Buffer - reader := io.TeeReader(file, &buf) + if _, err = io.Copy(&buf, file); err != nil { + httperror.BadRequest("could not read file", err, nil).Render(w) + return + } + + contactType, err := resolveReceiverContactType(bytes.NewReader(buf.Bytes())) + if err != nil { + errMsg := fmt.Sprintf("could not determine contact information type: %s", err) + httperror.BadRequest(errMsg, err, nil).Render(w) + return + } - instructions, v := parseInstructionsFromCSV(ctx, reader, disbursement.VerificationField) + instructions, v := parseInstructionsFromCSV(ctx, bytes.NewReader(buf.Bytes()), disbursement.VerificationField) if v != nil && v.HasErrors() { httperror.BadRequest("could not parse csv file", err, v.Errors).Render(w) return @@ -247,7 +257,14 @@ func (d DisbursementHandler) PostDisbursementInstructions(w http.ResponseWriter, return } - if err = d.Models.DisbursementInstructions.ProcessAll(ctx, user.ID, instructions, disbursement, disbursementUpdate, data.MaxInstructionsPerDisbursement); err != nil { + if err = d.Models.DisbursementInstructions.ProcessAll(ctx, data.DisbursementInstructionsOpts{ + UserID: user.ID, + Instructions: instructions, + ReceiverContactType: contactType, + Disbursement: disbursement, + DisbursementUpdate: disbursementUpdate, + MaxNumberOfInstructions: data.MaxInstructionsPerDisbursement, + }); 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) @@ -438,11 +455,12 @@ func (d DisbursementHandler) GetDisbursementInstructions(w http.ResponseWriter, } } -func parseInstructionsFromCSV(ctx context.Context, file io.Reader, verificationField data.VerificationType) ([]*data.DisbursementInstruction, *validators.DisbursementInstructionsValidator) { +// 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) instructions := []*data.DisbursementInstruction{} - if err := gocsv.Unmarshal(file, &instructions); err != nil { + if err := gocsv.Unmarshal(reader, &instructions); err != nil { log.Ctx(ctx).Errorf("error parsing csv file: %s", err.Error()) validator.Errors["file"] = "could not parse file" return nil, validator @@ -464,3 +482,34 @@ func parseInstructionsFromCSV(ctx context.Context, file io.Reader, verificationF return sanitizedInstructions, nil } + +// resolveReceiverContactType determines the type of contact information in the CSV file +func resolveReceiverContactType(file io.Reader) (data.ReceiverContactType, error) { + headers, err := csv.NewReader(file).Read() + if err != nil { + return "", fmt.Errorf("reading csv headers: %w", err) + } + + var hasPhone, hasEmail bool + for _, header := range headers { + switch strings.ToLower(strings.TrimSpace(header)) { + case "phone": + hasPhone = true + case "email": + hasEmail = 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") + } +} diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go index 7c14151a5..9709ab483 100644 --- a/internal/serve/httphandler/disbursement_handler_test.go +++ b/internal/serve/httphandler/disbursement_handler_test.go @@ -909,7 +909,7 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) { {}, }, expectedStatus: http.StatusBadRequest, - expectedMessage: "could not parse file", + expectedMessage: "could not determine contact information type", }, { name: "no instructions found in file", @@ -920,6 +920,24 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) { expectedStatus: http.StatusBadRequest, expectedMessage: "no valid instructions found", }, + { + name: "instructions invalid - attempting to upload phone and email", + disbursementID: draftDisbursement.ID, + csvRecords: [][]string{ + {"phone", "email", "id", "amount", "date-of-birth"}, + }, + 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, @@ -1159,7 +1177,7 @@ func Test_DisbursementHandler_GetDisbursementReceivers(t *testing.T) { { ID: receiver3.ID, PhoneNumber: receiver3.PhoneNumber, - Email: *receiver3.Email, + Email: receiver3.Email, ExternalID: receiver3.ExternalID, ReceiverWallet: receiverWallet3, Payment: payment3, @@ -1169,7 +1187,7 @@ func Test_DisbursementHandler_GetDisbursementReceivers(t *testing.T) { { ID: receiver2.ID, PhoneNumber: receiver2.PhoneNumber, - Email: *receiver2.Email, + Email: receiver2.Email, ExternalID: receiver2.ExternalID, ReceiverWallet: receiverWallet2, Payment: payment2, @@ -1179,7 +1197,7 @@ func Test_DisbursementHandler_GetDisbursementReceivers(t *testing.T) { { ID: receiver1.ID, PhoneNumber: receiver1.PhoneNumber, - Email: *receiver1.Email, + Email: receiver1.Email, ExternalID: receiver1.ExternalID, ReceiverWallet: receiverWallet1, Payment: payment1, diff --git a/internal/serve/httphandler/receiver_handler_test.go b/internal/serve/httphandler/receiver_handler_test.go index 754d2c3a9..b3ececed2 100644 --- a/internal/serve/httphandler/receiver_handler_test.go +++ b/internal/serve/httphandler/receiver_handler_test.go @@ -92,7 +92,7 @@ func Test_ReceiverHandlerGet(t *testing.T) { "remaining_payments": "0", "registered_wallets": "0", "wallets": [] - }`, receiver.ID, receiver.ExternalID, *receiver.Email, receiver.PhoneNumber, receiver.CreatedAt.Format(time.RFC3339Nano), receiver.UpdatedAt.Format(time.RFC3339Nano)) + }`, receiver.ID, receiver.ExternalID, receiver.Email, receiver.PhoneNumber, receiver.CreatedAt.Format(time.RFC3339Nano), receiver.UpdatedAt.Format(time.RFC3339Nano)) assert.JSONEq(t, wantJson, rr.Body.String()) }) @@ -196,7 +196,7 @@ func Test_ReceiverHandlerGet(t *testing.T) { "anchor_platform_transaction_id": %q } ] - }`, receiver.ID, receiver.ExternalID, *receiver.Email, receiver.PhoneNumber, receiver.CreatedAt.Format(time.RFC3339Nano), + }`, 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), @@ -340,7 +340,7 @@ func Test_ReceiverHandlerGet(t *testing.T) { "anchor_platform_transaction_id": %q } ] - }`, receiver.ID, receiver.ExternalID, *receiver.Email, receiver.PhoneNumber, receiver.CreatedAt.Format(time.RFC3339Nano), + }`, 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), @@ -505,7 +505,7 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) { date := time.Date(2022, 12, 10, 23, 40, 20, 1431, time.UTC) receiver1Email := "receiver1@mock.com" receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ - Email: &receiver1Email, + Email: receiver1Email, ExternalID: "external_id_1", PhoneNumber: "+99991111", CreatedAt: &date, @@ -515,7 +515,7 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) { date = time.Date(2023, 1, 10, 23, 40, 20, 1431, time.UTC) receiver2Email := "receiver2@mock.com" receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ - Email: &receiver2Email, + Email: receiver2Email, ExternalID: "external_id_2", PhoneNumber: "+99992222", CreatedAt: &date, @@ -546,7 +546,7 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) { date = time.Date(2023, 2, 10, 23, 40, 21, 1431, time.UTC) receiver3Email := "receiver3@mock.com" receiver3 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ - Email: &receiver3Email, + Email: receiver3Email, ExternalID: "external_id_3", PhoneNumber: "+99993333", CreatedAt: &date, @@ -575,9 +575,8 @@ func Test_ReceiverHandler_GetReceivers_Success(t *testing.T) { }) date = time.Date(2023, 3, 10, 23, 40, 20, 1431, time.UTC) - receiver4Email := "receiver4@mock.com" receiver4 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ - Email: &receiver4Email, + Email: "receiver4@mock.com", ExternalID: "external_id_4", PhoneNumber: "+99994444", CreatedAt: &date, @@ -1439,13 +1438,13 @@ func Test_ReceiverHandler_BuildReceiversResponse(t *testing.T) { receiver1Email := "receiver1@mock.com" receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ - Email: &receiver1Email, + Email: receiver1Email, ExternalID: "external_id_1", PhoneNumber: "+99991111", }) receiver2Email := "receiver2@mock.com" receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ - Email: &receiver2Email, + Email: receiver2Email, ExternalID: "external_id_2", PhoneNumber: "+99992222", }) diff --git a/internal/serve/httphandler/update_receiver_handler.go b/internal/serve/httphandler/update_receiver_handler.go index 0d84752d4..db2571ddb 100644 --- a/internal/serve/httphandler/update_receiver_handler.go +++ b/internal/serve/httphandler/update_receiver_handler.go @@ -97,13 +97,17 @@ func (h UpdateReceiverHandler) UpdateReceiver(rw http.ResponseWriter, req *http. } } - receiverUpdate := data.ReceiverUpdate{ - Email: reqBody.Email, - ExternalId: reqBody.ExternalID, + var receiverUpdate data.ReceiverUpdate + if reqBody.Email != "" { + receiverUpdate.Email = &reqBody.Email } - if receiverUpdate.Email != "" || receiverUpdate.ExternalId != "" { + if reqBody.ExternalID != "" { + receiverUpdate.ExternalId = &reqBody.ExternalID + } + + if !receiverUpdate.IsEmpty() { if innerErr = h.Models.Receiver.Update(ctx, dbTx, receiverID, receiverUpdate); innerErr != nil { - return nil, fmt.Errorf("error updating receiver with ID %s: %w", receiverID, innerErr) + return nil, fmt.Errorf("updating receiver with ID %s: %w", receiverID, innerErr) } } diff --git a/internal/serve/httphandler/update_receiver_handler_test.go b/internal/serve/httphandler/update_receiver_handler_test.go index 47b069403..d175f671a 100644 --- a/internal/serve/httphandler/update_receiver_handler_test.go +++ b/internal/serve/httphandler/update_receiver_handler_test.go @@ -117,7 +117,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { ctx := context.Background() receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ PhoneNumber: "+380445555555", - Email: &[]string{"receiver@email.com"}[0], + Email: "receiver@email.com", ExternalID: "externalID", }) @@ -305,7 +305,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, "receiver@email.com", *receiverDB.Email) + assert.Equal(t, "receiver@email.com", receiverDB.Email) assert.Equal(t, "externalID", receiverDB.ExternalID) }) @@ -349,7 +349,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, "receiver@email.com", *receiverDB.Email) + assert.Equal(t, "receiver@email.com", receiverDB.Email) assert.Equal(t, "externalID", receiverDB.ExternalID) }) @@ -393,7 +393,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, "receiver@email.com", *receiverDB.Email) + assert.Equal(t, "receiver@email.com", receiverDB.Email) assert.Equal(t, "externalID", receiverDB.ExternalID) }) @@ -437,7 +437,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, "receiver@email.com", *receiverDB.Email) + assert.Equal(t, "receiver@email.com", receiverDB.Email) assert.Equal(t, "externalID", receiverDB.ExternalID) }) @@ -533,7 +533,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, "receiver@email.com", *receiverDB.Email) + assert.Equal(t, "receiver@email.com", receiverDB.Email) assert.Equal(t, "externalID", receiverDB.ExternalID) } }) @@ -610,7 +610,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, "receiver@email.com", *receiverDB.Email) + assert.Equal(t, "receiver@email.com", receiverDB.Email) assert.Equal(t, "externalID", receiverDB.ExternalID) } }) @@ -635,7 +635,7 @@ func Test_UpdateReceiverHandler(t *testing.T) { receiverDB, err := models.Receiver.Get(ctx, dbConnectionPool, receiver.ID) require.NoError(t, err) - assert.Equal(t, "update_receiver@email.com", *receiverDB.Email) + assert.Equal(t, "update_receiver@email.com", receiverDB.Email) }) t.Run("updates receiver's external ID", func(t *testing.T) { diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler.go b/internal/serve/httphandler/verifiy_receiver_registration_handler.go index a6fe3a341..1ed600920 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler.go @@ -116,6 +116,7 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( receiverRegistrationRequest data.ReceiverRegistrationRequest, ) error { now := time.Now() + // TODO: SDP-1318 - Replace with correct contact depending on receiver choice. truncatedPhoneNumber := utils.TruncateString(receiver.PhoneNumber, 3) // STEP 1: find the receiverVerification entry that matches the pair [receiverID, verificationType] @@ -283,7 +284,7 @@ func (v VerifyReceiverRegistrationHandler) VerifyReceiverRegistration(w http.Res DBConnectionPool: v.Models.DBConnectionPool, AtomicFunctionWithPostCommit: func(dbTx db.DBTransaction) (postCommitFn db.PostCommitFunction, err error) { // STEP 2: find the receivers with the given phone number - receivers, err := v.Models.Receiver.GetByPhoneNumbers(ctx, dbTx, []string{receiverRegistrationRequest.PhoneNumber}) + receivers, err := v.Models.Receiver.GetByContacts(ctx, dbTx, receiverRegistrationRequest.PhoneNumber) if err != nil { err = fmt.Errorf("error retrieving receiver with phone number %s: %w", truncatedPhoneNumber, err) return nil, err diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go index b163aadcc..0af138841 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go @@ -491,7 +491,7 @@ func Test_VerifyReceiverRegistrationHandler_processAnchorPlatformID(t *testing.T handler := &VerifyReceiverRegistrationHandler{Models: models} // creeate fixtures - const phoneNumber = "+380445555555" + phoneNumber := "+380445555555" defer data.DeleteAllFixtures(t, ctx, dbConnectionPool) wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://home.page", "home.page", "wallet123://") receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) @@ -774,7 +774,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin }, } - const phoneNumber = "+380445555555" + phoneNumber := "+380445555555" receiverRegistrationRequest := data.ReceiverRegistrationRequest{ PhoneNumber: phoneNumber, OTP: "123456", diff --git a/internal/serve/validators/disbursement_instructions_validator.go b/internal/serve/validators/disbursement_instructions_validator.go index 63bc492b3..183fc8f54 100644 --- a/internal/serve/validators/disbursement_instructions_validator.go +++ b/internal/serve/validators/disbursement_instructions_validator.go @@ -21,17 +21,31 @@ func NewDisbursementInstructionsValidator(verificationField data.VerificationTyp } func (iv *DisbursementInstructionsValidator) ValidateInstruction(instruction *data.DisbursementInstruction, lineNumber int) { - phone := strings.TrimSpace(instruction.Phone) + 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 - iv.Check(phone != "", fmt.Sprintf("line %d - phone", lineNumber), "phone cannot be empty") if phone != "" { iv.CheckError(utils.ValidatePhoneNumber(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") @@ -53,14 +67,21 @@ func (iv *DisbursementInstructionsValidator) ValidateInstruction(instruction *da func (iv *DisbursementInstructionsValidator) SanitizeInstruction(instruction *data.DisbursementInstruction) *data.DisbursementInstruction { var sanitizedInstruction data.DisbursementInstruction - sanitizedInstruction.Phone = strings.TrimSpace(instruction.Phone) + if instruction.Phone != "" { + sanitizedInstruction.Phone = strings.ToLower(strings.TrimSpace(instruction.Phone)) + } + + if instruction.Email != "" { + sanitizedInstruction.Email = strings.ToLower(strings.TrimSpace(instruction.Email)) + } + + if instruction.ExternalPaymentId != "" { + sanitizedInstruction.ExternalPaymentId = strings.TrimSpace(instruction.ExternalPaymentId) + } + sanitizedInstruction.ID = strings.TrimSpace(instruction.ID) sanitizedInstruction.Amount = strings.TrimSpace(instruction.Amount) sanitizedInstruction.VerificationValue = strings.TrimSpace(instruction.VerificationValue) - if instruction.ExternalPaymentId != nil { - externalPaymentId := strings.TrimSpace(*instruction.ExternalPaymentId) - sanitizedInstruction.ExternalPaymentId = &externalPaymentId - } return &sanitizedInstruction } diff --git a/internal/serve/validators/disbursement_instructions_validator_test.go b/internal/serve/validators/disbursement_instructions_validator_test.go index 36c4deefd..cdded4201 100644 --- a/internal/serve/validators/disbursement_instructions_validator_test.go +++ b/internal/serve/validators/disbursement_instructions_validator_test.go @@ -18,7 +18,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing expectedErrors map[string]interface{} }{ { - name: "error if phone number is empty", + name: "error if phone number and email are empty", instruction: &data.DisbursementInstruction{ ID: "123456789", Amount: "100.5", @@ -28,7 +28,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing verificationField: data.VerificationTypeDateOfBirth, hasErrors: true, expectedErrors: map[string]interface{}{ - "line 2 - phone": "phone cannot be empty", + "line 2 - contact": "phone or email must be provided", }, }, { @@ -41,7 +41,7 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing "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 - phone": "phone cannot be empty", + "line 2 - contact": "phone or email must be provided", }, }, { @@ -74,6 +74,21 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing "line 3 - amount": "invalid amount. Amount must be a positive number", }, }, + { + name: "error if email is not valid", + instruction: &data.DisbursementInstruction{ + Email: "invalidemail", + ID: "123456789", + Amount: "100.5", + VerificationValue: "1990-01-01", + }, + lineNumber: 3, + verificationField: data.VerificationTypeDateOfBirth, + hasErrors: true, + expectedErrors: map[string]interface{}{ + "line 3 - email": "invalid email format", + }, + }, { name: "error if amount is not positive", instruction: &data.DisbursementInstruction{ @@ -243,6 +258,30 @@ func Test_DisbursementInstructionsValidator_ValidateAndGetInstruction(t *testing verificationField: data.VerificationTypePin, hasErrors: false, }, + { + name: "๐ŸŽ‰ successfully validates instructions (Email)", + instruction: &data.DisbursementInstruction{ + Email: "myemail@stellar.org", + ID: "123456789", + Amount: "100.5", + VerificationValue: "1234", + }, + lineNumber: 3, + verificationField: data.VerificationTypePin, + hasErrors: false, + }, + { + name: "๐ŸŽ‰ successfully validates instructions (Phone)", + instruction: &data.DisbursementInstruction{ + Phone: "+380445555555", + ID: "123456789", + Amount: "100.5", + VerificationValue: "1234", + }, + lineNumber: 3, + verificationField: data.VerificationTypePin, + hasErrors: false, + }, } for _, tt := range tests { @@ -280,7 +319,7 @@ func Test_DisbursementInstructionsValidator_SanitizeInstruction(t *testing.T) { ID: "123456789", Amount: "100.5", VerificationValue: "1990-01-01", - ExternalPaymentId: nil, + ExternalPaymentId: "", }, }, { @@ -290,14 +329,29 @@ func Test_DisbursementInstructionsValidator_SanitizeInstruction(t *testing.T) { ID: " 123456789 ", Amount: " 100.5 ", VerificationValue: " 1990-01-01 ", - ExternalPaymentId: &externalPaymentIDWithSpaces, + ExternalPaymentId: externalPaymentIDWithSpaces, }, expectedInstruction: &data.DisbursementInstruction{ Phone: "+380445555555", ID: "123456789", Amount: "100.5", VerificationValue: "1990-01-01", - ExternalPaymentId: &externalPaymentID, + ExternalPaymentId: externalPaymentID, + }, + }, + { + name: "Sanitized instruction with email", + actual: &data.DisbursementInstruction{ + Email: " MyEmail@stellar.org ", + ID: " 123456789 ", + Amount: " 100.5 ", + VerificationValue: " 1990-01-01 ", + }, + expectedInstruction: &data.DisbursementInstruction{ + Email: "myemail@stellar.org", + ID: "123456789", + Amount: "100.5", + VerificationValue: "1990-01-01", }, }, } diff --git a/internal/services/send_receiver_wallets_invite_service.go b/internal/services/send_receiver_wallets_invite_service.go index 8a745016f..3e3c6d05e 100644 --- a/internal/services/send_receiver_wallets_invite_service.go +++ b/internal/services/send_receiver_wallets_invite_service.go @@ -155,9 +155,15 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive return fmt.Errorf("executing registration message template: %w", err) } + // TODO: SDP-1316 - add a Title. Consider if we should make the title configurable. msg := message.Message{ - ToPhoneNumber: rwa.ReceiverWallet.Receiver.PhoneNumber, - Message: content.String(), + Message: content.String(), + } + if rwa.ReceiverWallet.Receiver.PhoneNumber != "" { + msg.ToPhoneNumber = rwa.ReceiverWallet.Receiver.PhoneNumber + } + if rwa.ReceiverWallet.Receiver.Email != "" { + msg.ToEmail = rwa.ReceiverWallet.Receiver.Email } assetID := rwa.Asset.ID @@ -183,12 +189,12 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive // We assume that the message will be sent at first msgToInsert.Status = data.SuccessMessageStatus if err := s.messageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority); err != nil { - msg := fmt.Sprintf( + errMsg := fmt.Sprintf( "error sending message to receiver ID %s for receiver wallet ID %s using messenger type %s", rwa.ReceiverWallet.Receiver.ID, rwa.ReceiverWallet.ID, messageType, ) // call crash tracker client to log and report error - s.crashTrackerClient.LogAndReportErrors(ctx, err, msg) + s.crashTrackerClient.LogAndReportErrors(ctx, err, errMsg) msgToInsert.Status = data.FailureMessageStatus } @@ -239,11 +245,20 @@ func (s SendReceiverWalletInviteService) resolveReceiverWalletsPendingRegistrati } // shouldSendInvitationSMS returns true if we should send the invitation SMS to the receiver. It will be used to either -// send the invitation for the first time, or to resend it automatically according with the organization's SMS Resend +// send the invitation for the first time, or to resend it automatically according to the organization's SMS Resend // Interval and the maximum number of SMS resend attempts. - func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Context, organization *data.Organization, rwa *data.ReceiverWalletAsset) bool { - truncatedPhoneNumber := utils.TruncateString(rwa.ReceiverWallet.Receiver.PhoneNumber, 3) + receiver := rwa.ReceiverWallet.Receiver + + // TODO: SDP-1316 - add support for other contact information in this method. + var phoneNumber string + if receiver.PhoneNumber == "" { + return false + } else { + phoneNumber = receiver.PhoneNumber + } + + truncatedReceiverContact := utils.TruncateString(phoneNumber, 3) // We've never sent a Invitation SMS if rwa.ReceiverWallet.InvitationSentAt == nil { @@ -254,7 +269,7 @@ func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Con if organization.SMSResendInterval == nil && rwa.ReceiverWallet.InvitationSentAt != nil { log.Ctx(ctx).Debugf( "the invitation message was not automatically resent to the receiver %s with phone number %s because the organization's SMS Resend Interval is nil", - rwa.ReceiverWallet.Receiver.ID, truncatedPhoneNumber) + receiver.ID, truncatedReceiverContact) return false } @@ -264,8 +279,8 @@ func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Con if rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationSMSResentAttempts >= s.maxInvitationSMSResendAttempts { log.Ctx(ctx).Debugf( "the invitation message was not resent to the receiver because the maximum number of SMS resend attempts has been reached: Phone Number: %s - Receiver ID %s - Wallet ID %s - Total Invitation SMS resent %d - Maximum attempts %d", - truncatedPhoneNumber, - rwa.ReceiverWallet.Receiver.ID, + truncatedReceiverContact, + receiver.ID, rwa.WalletID, rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationSMSResentAttempts, s.maxInvitationSMSResendAttempts, @@ -279,8 +294,8 @@ func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Con if !rwa.ReceiverWallet.InvitationSentAt.Before(resendPeriod) { log.Ctx(ctx).Debugf( "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Phone Number: %s - Receiver ID %s - Wallet ID %s - Last Invitation Sent At %s - SMS Resend Interval %d day(s)", - truncatedPhoneNumber, - rwa.ReceiverWallet.Receiver.ID, + truncatedReceiverContact, + receiver.ID, rwa.WalletID, rwa.ReceiverWallet.InvitationSentAt.Format(time.RFC1123), *organization.SMSResendInterval, diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go index cf5a32295..afedec71a 100644 --- a/internal/services/send_receiver_wallets_invite_service_test.go +++ b/internal/services/send_receiver_wallets_invite_service_test.go @@ -163,12 +163,14 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, + ToEmail: receiver1.Email, Message: contentWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(errors.New("unexpected error")). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, + ToEmail: receiver2.Email, Message: contentWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). @@ -302,12 +304,14 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, + ToEmail: receiver1.Email, Message: contentWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, + ToEmail: receiver2.Email, Message: contentWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). @@ -437,12 +441,14 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, + ToEmail: receiver1.Email, Message: contentWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, + ToEmail: receiver2.Email, Message: contentWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). @@ -730,6 +736,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, + ToEmail: receiver1.Email, Message: contentWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). @@ -848,12 +855,14 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, + ToEmail: receiver1.Email, Message: contentDisbursement3, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, + ToEmail: receiver2.Email, Message: contentDisbursement4, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). @@ -977,6 +986,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, + ToEmail: receiver1.Email, Message: contentDisbursement, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). Return(nil). @@ -1038,6 +1048,9 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) rwa := data.ReceiverWalletAsset{ ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: nil, + Receiver: data.Receiver{ + PhoneNumber: "+380443973607", + }, }, } got := s.shouldSendInvitationSMS(ctx, &org, &rwa) @@ -1135,6 +1148,9 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) ReceiverWalletStats: data.ReceiverWalletStats{ TotalInvitationSMSResentAttempts: 0, }, + Receiver: data.Receiver{ + PhoneNumber: "+380443973607", + }, }, } got := s.shouldSendInvitationSMS(ctx, &org, &rwa) @@ -1148,6 +1164,9 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) ReceiverWalletStats: data.ReceiverWalletStats{ TotalInvitationSMSResentAttempts: 1, }, + Receiver: data.Receiver{ + PhoneNumber: "+380443973607", + }, }, } got = s.shouldSendInvitationSMS(ctx, &org, &rwa) @@ -1161,6 +1180,9 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) ReceiverWalletStats: data.ReceiverWalletStats{ TotalInvitationSMSResentAttempts: 2, }, + Receiver: data.Receiver{ + PhoneNumber: "+380443973607", + }, }, } got = s.shouldSendInvitationSMS(ctx, &org, &rwa) @@ -1174,6 +1196,9 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) ReceiverWalletStats: data.ReceiverWalletStats{ TotalInvitationSMSResentAttempts: 3, }, + Receiver: data.Receiver{ + PhoneNumber: "+380443973607", + }, }, } got = s.shouldSendInvitationSMS(ctx, &org, &rwa) From bededfec4fee3b7fea06b599cd92fa3994bf109d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 22:32:26 +0000 Subject: [PATCH 23/75] Bump golang in the all-docker group (#414) --- Dockerfile | 2 +- Dockerfile.development | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 95c2c2bd3..821dcc658 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # To push: # make docker-push -FROM golang:1.23.0-bullseye AS build +FROM golang:1.23.1-bullseye AS build ARG GIT_COMMIT WORKDIR /src/stellar-disbursement-platform diff --git a/Dockerfile.development b/Dockerfile.development index 7265e6bb4..47eb05049 100644 --- a/Dockerfile.development +++ b/Dockerfile.development @@ -1,5 +1,5 @@ # Stage 1: Build the Go application -FROM golang:1.23.0-bullseye AS build +FROM golang:1.23.1-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.0-bullseye AS development +FROM golang:1.23.1-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 82eb14f42f6fdf24dd2b28e90fb56796b584a488 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:07:34 +0000 Subject: [PATCH 24/75] Bump the minor-and-patch group across 1 directory with 5 updates (#411) --- go.list | 16 ++++++++-------- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/go.list b/go.list index 95c8bd5e7..f903e4302 100644 --- a/go.list +++ b/go.list @@ -69,7 +69,7 @@ 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 github.com/go-chi/chi/v5 v5.1.0 -github.com/go-chi/httprate v0.14.0 +github.com/go-chi/httprate v0.14.1 github.com/go-errors/errors v1.5.1 github.com/go-gorp/gorp/v3 v3.1.0 github.com/go-kit/log v0.2.1 @@ -201,13 +201,13 @@ 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.2 +github.com/prometheus/client_golang v1.20.3 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.55.0 github.com/prometheus/procfs v0.15.1 github.com/rivo/uniseg v0.2.0 github.com/rogpeppe/go-internal v1.11.0 -github.com/rs/cors v1.11.0 +github.com/rs/cors v1.11.1 github.com/rubenv/sql-migrate v1.7.0 github.com/russross/blackfriday/v2 v2.1.0 github.com/sagikazarmark/crypt v0.19.0 @@ -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.22.3 +github.com/twilio/twilio-go v1.23.0 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 +272,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.26.0 +golang.org/x/crypto v0.27.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.23.0 -golang.org/x/term v0.23.0 -golang.org/x/text v0.17.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/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 d65b33301..dc0586f4f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/getsentry/sentry-go v0.28.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.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/google/uuid v1.6.0 @@ -19,8 +19,8 @@ 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.2 - github.com/rs/cors v1.11.0 + github.com/prometheus/client_golang v1.20.3 + 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/sirupsen/logrus v1.9.3 @@ -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.22.3 - golang.org/x/crypto v0.26.0 + github.com/twilio/twilio-go v1.23.0 + golang.org/x/crypto v0.27.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.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.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 ee0656e6a..643c1939d 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyN 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= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/httprate v0.14.0 h1:c8szLJc+Gn+1EC1jjv3q88Om4a9USAqU9lL8wQFVX2M= -github.com/go-chi/httprate v0.14.0/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= +github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs= +github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= @@ -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.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= -github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +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_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= @@ -148,8 +148,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rubenv/sql-migrate v1.7.0 h1:HtQq1xyTN2ISmQDggnh0c9U3JlP8apWh8YO2jzlXpTI= github.com/rubenv/sql-migrate v1.7.0/go.mod h1:S4wtDEG1CKn+0ShpTtzWhFpHHI5PvCUtiGI+C+Z2THE= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -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.22.3 h1:u+h5ywaFd2kGO/36PkizX4N/g5q842cjQQcqZqm6rCo= -github.com/twilio/twilio-go v1.22.3/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= +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/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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +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/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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.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 a24a4e6b9481be63a4fe5a3aabea5a6ab83ef9f3 Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Mon, 9 Sep 2024 19:12:36 -0700 Subject: [PATCH 25/75] [SDP-1316] rename SMS related fields in `organization` and `disbursement` (#412) * SDP-1316 Rename `shouldSendInvitationSMS` to `shouldSendInvitation` * SDP-1316 Rename sms related fields in organizations and disbursements * SDP-1316 Rename `receiver_invitation_resend_interval` to `receiver_invitation_resend_interval_days` * SDP-1316 Rename variables mentioning SMS * SDP-1316 Fix helm chart readme --- CHANGELOG.md | 7 +- README.md | 2 +- cmd/serve.go | 40 +++--- cmd/serve_test.go | 2 +- ...9-06.0-change-organization-sms-columns.sql | 38 ++++++ helmchart/sdp/README.md | 2 +- helmchart/sdp/values.yaml | 4 +- internal/data/assets.go | 14 +- internal/data/assets_test.go | 90 ++++++------- internal/data/disbursement_instructions.go | 2 +- internal/data/disbursements.go | 36 +++--- internal/data/disbursements_test.go | 12 +- internal/data/organizations.go | 62 ++++----- internal/data/organizations_test.go | 42 +++--- internal/data/receivers_wallet.go | 8 +- internal/data/receivers_wallet_test.go | 6 +- ...eiver_wallets_invitation_event_handler.go} | 36 +++--- ..._wallets_invitation_event_handler_test.go} | 12 +- internal/events/handler.go | 4 +- internal/events/schemas/schemas.go | 2 +- ...end_receiver_wallets_sms_invitation_job.go | 42 +++--- ...eceiver_wallets_sms_invitation_job_test.go | 34 ++--- internal/scheduler/scheduler.go | 4 +- .../serve/httphandler/disbursement_handler.go | 22 ++-- .../httphandler/disbursement_handler_test.go | 14 +- .../httphandler/payments_handler_test.go | 2 +- internal/serve/httphandler/profile_handler.go | 70 +++++----- .../serve/httphandler/profile_handler_test.go | 84 ++++++------ .../httphandler/receiver_wallets_handler.go | 12 +- .../receiver_wallets_handler_test.go | 12 +- internal/serve/serve.go | 2 +- .../disbursement_management_service.go | 6 +- .../disbursement_management_service_test.go | 18 +-- .../send_receiver_wallets_invite_service.go | 2 +- .../send_receiver_wallets_invite_service.go | 98 +++++++------- ...nd_receiver_wallets_invite_service_test.go | 120 +++++++++--------- 36 files changed, 503 insertions(+), 460 deletions(-) create mode 100644 db/migrations/sdp-migrations/2024-09-06.0-change-organization-sms-columns.sql rename internal/events/eventhandlers/{send_receiver_wallets_sms_invitation_event_handler.go => send_receiver_wallets_invitation_event_handler.go} (63%) rename internal/events/eventhandlers/{send_receiver_wallets_sms_invitation_event_handler_test.go => send_receiver_wallets_invitation_event_handler_test.go} (90%) diff --git a/CHANGELOG.md b/CHANGELOG.md index aea432dc9..b4c7fb00d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ## Unreleased -None +### 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` + - Renamed `organization.sms_resend_interval` to `organization.receiver_invitation_resend_interval_days` + - Renamed `organization.sms_registration_message_template` to `organization.receiver_registration_message_template` + - Renamed `disbursement.sms_registration_message_template` to `disbursement.receiver_registration_message_template` ## [2.1.0](https://github.com/stellar/stellar-disbursement-platform-backend/releases/tag/2.1.0) ([diff](https://github.com/stellar/stellar-disbursement-platform-backend/compare/2.0.0...2.1.0)) diff --git a/README.md b/README.md index 8f6430b55..85d248359 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ We recommend Background Jobs for organizations that require a simpler setup and > [!NOTE] > Certain jobs are not listed here because they cannot be configured and are necessary to the functioning of the SDP. -* `send_receiver_wallets_sms_invitation_job`: This job is used to send disbursement invites to recipients. Its interval is configured through the `SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS` environment variable. +* `send_receiver_wallets_invitation_job`: This job is used to send disbursement invites to recipients. Its interval is configured through the `SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS` environment variable. * `payment_to_submitter_job`: This job is used to submit payments from Core to the TSS. Its interval is configured through the `SCHEDULER_PAYMENT_JOB_SECONDS` environment variable. * `payment_from_submitter_job`: This job is used to notify Core that a payment has been completed. Its interval is configured through the `SCHEDULER_PAYMENT_JOB_SECONDS` environment variable. * `patch_anchor_platform_transactions_completion`: This job is used to patch transactions in Anchor Platform once payments reach the final state 'SUCCESS' or 'FAILED'. Its interval is configured through the `SCHEDULER_PAYMENT_JOB_SECONDS` environment variable. diff --git a/cmd/serve.go b/cmd/serve.go index 87ccb2a1b..ee96e1b68 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -116,13 +116,13 @@ func (s *ServerService) GetSchedulerJobRegistrars( }), scheduler.WithPaymentFromSubmitterJobOption(schedulerOptions.PaymentJobIntervalSeconds, models, tssDBConnectionPool), scheduler.WithPatchAnchorPlatformTransactionsCompletionJobOption(schedulerOptions.PaymentJobIntervalSeconds, apAPIService, models), - scheduler.WithSendReceiverWalletsSMSInvitationJobOption(jobs.SendReceiverWalletsSMSInvitationJobOptions{ - Models: models, - MessageDispatcher: serveOpts.MessageDispatcher, - MaxInvitationSMSResendAttempts: int64(serveOpts.MaxInvitationSMSResendAttempts), - Sep10SigningPrivateKey: serveOpts.Sep10SigningPrivateKey, - CrashTrackerClient: serveOpts.CrashTrackerClient.Clone(), - JobIntervalSeconds: schedulerOptions.ReceiverInvitationJobIntervalSeconds, + scheduler.WithSendReceiverWalletsInvitationJobOption(jobs.SendReceiverWalletsInvitationJobOptions{ + Models: models, + MessageDispatcher: serveOpts.MessageDispatcher, + MaxInvitationResendAttempts: int64(serveOpts.MaxInvitationResendAttempts), + Sep10SigningPrivateKey: serveOpts.Sep10SigningPrivateKey, + CrashTrackerClient: serveOpts.CrashTrackerClient.Clone(), + JobIntervalSeconds: schedulerOptions.ReceiverInvitationJobIntervalSeconds, }), ) } @@ -139,21 +139,21 @@ type SetupConsumersOptions struct { func (s *ServerService) SetupConsumers(ctx context.Context, o SetupConsumersOptions) error { kafkaConfig := cmdUtils.KafkaConfig(o.EventBrokerOptions) - smsInvitationConsumer, err := events.NewKafkaConsumer( + receiverInvitationConsumer, err := events.NewKafkaConsumer( kafkaConfig, events.ReceiverWalletNewInvitationTopic, o.EventBrokerOptions.ConsumerGroupID, - eventhandlers.NewSendReceiverWalletsSMSInvitationEventHandler(eventhandlers.SendReceiverWalletsSMSInvitationEventHandlerOptions{ - MtnDBConnectionPool: o.ServeOpts.MtnDBConnectionPool, - AdminDBConnectionPool: o.ServeOpts.AdminDBConnectionPool, - AnchorPlatformBaseSepURL: o.ServeOpts.AnchorPlatformBasePlatformURL, - MessageDispatcher: o.ServeOpts.MessageDispatcher, - MaxInvitationSMSResendAttempts: int64(o.ServeOpts.MaxInvitationSMSResendAttempts), - Sep10SigningPrivateKey: o.ServeOpts.Sep10SigningPrivateKey, + eventhandlers.NewSendReceiverWalletsInvitationEventHandler(eventhandlers.SendReceiverWalletsInvitationEventHandlerOptions{ + MtnDBConnectionPool: o.ServeOpts.MtnDBConnectionPool, + AdminDBConnectionPool: o.ServeOpts.AdminDBConnectionPool, + AnchorPlatformBaseSepURL: o.ServeOpts.AnchorPlatformBasePlatformURL, + MessageDispatcher: o.ServeOpts.MessageDispatcher, + MaxInvitationResendAttempts: int64(o.ServeOpts.MaxInvitationResendAttempts), + Sep10SigningPrivateKey: o.ServeOpts.Sep10SigningPrivateKey, }), ) if err != nil { - return fmt.Errorf("creating SMS Invitation Kafka Consumer: %w", err) + return fmt.Errorf("creating Receiver Invitation Kafka Consumer: %w", err) } paymentCompletedConsumer, err := events.NewKafkaConsumer( @@ -212,7 +212,7 @@ func (s *ServerService) SetupConsumers(ctx context.Context, o SetupConsumersOpti return fmt.Errorf("creating Kafka producer: %w", err) } - go events.NewEventConsumer(smsInvitationConsumer, producer, o.ServeOpts.CrashTrackerClient.Clone()).Consume(ctx) + go events.NewEventConsumer(receiverInvitationConsumer, producer, o.ServeOpts.CrashTrackerClient.Clone()).Consume(ctx) go events.NewEventConsumer(paymentCompletedConsumer, producer, o.ServeOpts.CrashTrackerClient.Clone()).Consume(ctx) go events.NewEventConsumer(stellarPaymentReadyToPayConsumer, producer, o.ServeOpts.CrashTrackerClient.Clone()).Consume(ctx) go events.NewEventConsumer(circlePaymentReadyToPayConsumer, producer, o.ServeOpts.CrashTrackerClient.Clone()).Consume(ctx) @@ -356,10 +356,10 @@ func (c *ServeCommand) Command(serverService ServerServiceInterface, monitorServ Required: false, }, { - Name: "max-invitation-sms-resend-attempts", - Usage: "The maximum number of attempts to resend the SMS invitation to the Receiver Wallets.", + Name: "max-invitation-resend-attempts", + Usage: "The maximum number of attempts to resend the invitation to the Receiver Wallets.", OptType: types.Int, - ConfigKey: &serveOpts.MaxInvitationSMSResendAttempts, + ConfigKey: &serveOpts.MaxInvitationResendAttempts, FlagDefault: 3, Required: true, }, diff --git a/cmd/serve_test.go b/cmd/serve_test.go index 05a4ac038..3b23d0d9c 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -175,7 +175,7 @@ func Test_serve(t *testing.T) { EnableScheduler: false, SubmitterEngine: submitterEngine, DistributionAccountService: mDistAccService, - MaxInvitationSMSResendAttempts: 3, + MaxInvitationResendAttempts: 3, DistAccEncryptionPassphrase: distributionAccPrivKey, CircleService: mCircleService, } diff --git a/db/migrations/sdp-migrations/2024-09-06.0-change-organization-sms-columns.sql b/db/migrations/sdp-migrations/2024-09-06.0-change-organization-sms-columns.sql new file mode 100644 index 000000000..a06aea5c4 --- /dev/null +++ b/db/migrations/sdp-migrations/2024-09-06.0-change-organization-sms-columns.sql @@ -0,0 +1,38 @@ +-- +migrate Up +-- Rename organizations.sms_resend_interval to organizations.receiver_invitation_resend_interval +ALTER TABLE organizations + RENAME COLUMN sms_resend_interval TO receiver_invitation_resend_interval_days; + +-- Rename organizations.sms_registration_message_template to organizations.receiver_registration_message_template +ALTER TABLE organizations + RENAME COLUMN sms_registration_message_template TO receiver_registration_message_template; + +-- Update the constraint name for resend interval +ALTER TABLE organizations + DROP CONSTRAINT organization_sms_resend_interval_valid_value_check; + +ALTER TABLE organizations + ADD CONSTRAINT organization_invitation_resend_interval_valid_value_check + CHECK ((receiver_invitation_resend_interval_days IS NOT NULL AND receiver_invitation_resend_interval_days > 0) OR receiver_invitation_resend_interval_days IS NULL); + +-- Rename disbursements.sms_registration_message_template to disbursements.receiver_registration_message_template +ALTER TABLE disbursements + RENAME COLUMN sms_registration_message_template TO receiver_registration_message_template; + + +-- +migrate Down +ALTER TABLE organizations + RENAME COLUMN receiver_invitation_resend_interval_days TO sms_resend_interval; + +ALTER TABLE organizations + RENAME COLUMN receiver_registration_message_template TO sms_registration_message_template; + +ALTER TABLE organizations + DROP CONSTRAINT organization_invitation_resend_interval_valid_value_check; + +ALTER TABLE organizations + ADD CONSTRAINT organization_sms_resend_interval_valid_value_check + CHECK ((sms_resend_interval IS NOT NULL AND sms_resend_interval > 0) OR sms_resend_interval IS NULL); + +ALTER TABLE disbursements + RENAME COLUMN receiver_registration_message_template TO sms_registration_message_template; diff --git a/helmchart/sdp/README.md b/helmchart/sdp/README.md index 31e3a866a..690381208 100644 --- a/helmchart/sdp/README.md +++ b/helmchart/sdp/README.md @@ -137,7 +137,7 @@ Configuration parameters for the SDP Core Service which is the core backend serv | `sdp.configMap.data.ENABLE_SCHEDULER` | Whether the scheduled jobs are enabled in this instance ("true" or "false"). Default "false". | `false` | | `sdp.configMap.data.SCHEDULER_PAYMENT_JOB_SECONDS` | The interval in seconds for the payment job that syncs payments between the SDP and the TSS. | `3600` | | `sdp.configMap.data.SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS` | The interval in seconds for the receiver invitation job that sends invitations to new receivers. 0 or negative values disable the job. | `3600` | -| `sdp.configMap.data.MAX_INVITATION_SMS_RESEND_ATTEMPTS` | The maximum number of times an invitation SMS can be resent. 0 or negative values disable the job. | `3` | +| `sdp.configMap.data.MAX_INVITATION_RESEND_ATTEMPTS` | The maximum number of times an invitation can be resent. 0 or negative values disable the job. | `3` | | `sdp.configMap.data.TENANT_XLM_BOOTSTRAP_AMOUNT` | The amount of XLM to be sent to a newly created tenant distribution account. | `5` | | `sdp.kubeSecrets` | Kubernetes secrets are used to manage sensitive information, such as API keys and private keys. It's crucial that these details are kept private. | | | `sdp.kubeSecrets.secretName` | The name of the Kubernetes secret object. Only use this if create is false. | `sdp-backend-secret-name` | diff --git a/helmchart/sdp/values.yaml b/helmchart/sdp/values.yaml index 1315287b4..58e7bd77e 100644 --- a/helmchart/sdp/values.yaml +++ b/helmchart/sdp/values.yaml @@ -153,7 +153,7 @@ sdp: ## @param sdp.configMap.data.ENABLE_SCHEDULER Whether the scheduled jobs are enabled in this instance ("true" or "false"). Default "false". ## @param sdp.configMap.data.SCHEDULER_PAYMENT_JOB_SECONDS The interval in seconds for the payment job that syncs payments between the SDP and the TSS. ## @param sdp.configMap.data.SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS The interval in seconds for the receiver invitation job that sends invitations to new receivers. 0 or negative values disable the job. - ## @param sdp.configMap.data.MAX_INVITATION_SMS_RESEND_ATTEMPTS The maximum number of times an invitation SMS can be resent. 0 or negative values disable the job. + ## @param sdp.configMap.data.MAX_INVITATION_RESEND_ATTEMPTS The maximum number of times an invitation can be resent. 0 or negative values disable the job. ## @param sdp.configMap.data.TENANT_XLM_BOOTSTRAP_AMOUNT The amount of XLM to be sent to a newly created tenant distribution account. configMap: annotations: @@ -176,7 +176,7 @@ sdp: ENABLE_SCHEDULER: "false" SCHEDULER_PAYMENT_JOB_SECONDS: "3600" SCHEDULER_RECEIVER_INVITATION_JOB_SECONDS: "3600" - MAX_INVITATION_SMS_RESEND_ATTEMPTS: "3" + MAX_INVITATION_RESEND_ATTEMPTS: "3" TENANT_XLM_BOOTSTRAP_AMOUNT: "5" ## @extra sdp.kubeSecrets Kubernetes secrets are used to manage sensitive information, such as API keys and private keys. It's crucial that these details are kept private. diff --git a/internal/data/assets.go b/internal/data/assets.go index 3bc5f6071..8ec6e3834 100644 --- a/internal/data/assets.go +++ b/internal/data/assets.go @@ -224,10 +224,10 @@ func (a *AssetModel) SoftDelete(ctx context.Context, sqlExec db.SQLExecuter, id } type ReceiverWalletAsset struct { - WalletID string `db:"wallet_id"` - ReceiverWallet ReceiverWallet `db:"receiver_wallet"` - Asset Asset `db:"asset"` - DisbursementSMSTemplate *string `json:"-" db:"sms_registration_message_template"` + WalletID string `db:"wallet_id"` + ReceiverWallet ReceiverWallet `db:"receiver_wallet"` + Asset Asset `db:"asset"` + DisbursementReceiverRegistrationMsgTemplate *string `json:"-" db:"receiver_registration_message_template"` } // GetAssetsPerReceiverWallet returns the assets associated with a READY payment for each receiver @@ -245,7 +245,7 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal SELECT p.id AS payment_id, d.wallet_id, - COALESCE(d.sms_registration_message_template, '') as sms_registration_message_template, + COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template, p.asset_id FROM payments p @@ -254,7 +254,7 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal WHERE p.status = $1 GROUP BY - p.id, p.asset_id, d.wallet_id, d.sms_registration_message_template + p.id, p.asset_id, d.wallet_id, d.receiver_registration_message_template ORDER BY p.updated_at DESC ), messages_resent_since_invitation AS ( @@ -279,7 +279,7 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal ) SELECT DISTINCT lpw.wallet_id, - lpw.sms_registration_message_template, + lpw.receiver_registration_message_template, rw.id AS "receiver_wallet.id", rw.invitation_sent_at AS "receiver_wallet.invitation_sent_at", COALESCE(mrsi.total_invitation_sms_resent_attempts, 0) AS "receiver_wallet.total_invitation_sms_resent_attempts", diff --git a/internal/data/assets_test.go b/internal/data/assets_test.go index fd4b9fd07..67e23d8cf 100644 --- a/internal/data/assets_test.go +++ b/internal/data/assets_test.go @@ -460,32 +460,32 @@ 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, - SMSRegistrationMessageTemplate: "Disbursement SMS Registration Message Template A1", + 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, - SMSRegistrationMessageTemplate: "Disbursement SMS Registration Message Template A2", + 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, - SMSRegistrationMessageTemplate: "Disbursement SMS Registration Message Template B1", + 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, - SMSRegistrationMessageTemplate: "Disbursement SMS Registration Message Template B2", + Country: country, + Wallet: walletB, + Status: ReadyDisbursementStatus, + Asset: asset2, + ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template B2", }) // 2. Create receivers, and receiver wallets: @@ -642,13 +642,13 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { PhoneNumber: receiverX.PhoneNumber, }, ReceiverWalletStats: ReceiverWalletStats{ - TotalInvitationSMSResentAttempts: 2, + TotalInvitationResentAttempts: 2, }, InvitationSentAt: &invitationSentAt, }, - WalletID: walletA.ID, - Asset: *asset1, - DisbursementSMSTemplate: &disbursementA1.SMSRegistrationMessageTemplate, + WalletID: walletA.ID, + Asset: *asset1, + DisbursementReceiverRegistrationMsgTemplate: &disbursementA1.ReceiverRegistrationMessageTemplate, }, { ReceiverWallet: ReceiverWallet{ @@ -660,9 +660,9 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { }, InvitationSentAt: &invitationSentAt, }, - WalletID: walletA.ID, - Asset: *asset2, - DisbursementSMSTemplate: &disbursementA2.SMSRegistrationMessageTemplate, + WalletID: walletA.ID, + Asset: *asset2, + DisbursementReceiverRegistrationMsgTemplate: &disbursementA2.ReceiverRegistrationMessageTemplate, }, { ReceiverWallet: ReceiverWallet{ @@ -673,9 +673,9 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { PhoneNumber: receiverX.PhoneNumber, }, }, - WalletID: walletB.ID, - Asset: *asset1, - DisbursementSMSTemplate: &disbursementB1.SMSRegistrationMessageTemplate, + WalletID: walletB.ID, + Asset: *asset1, + DisbursementReceiverRegistrationMsgTemplate: &disbursementB1.ReceiverRegistrationMessageTemplate, }, { ReceiverWallet: ReceiverWallet{ @@ -686,9 +686,9 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { PhoneNumber: receiverX.PhoneNumber, }, }, - WalletID: walletB.ID, - Asset: *asset2, - DisbursementSMSTemplate: &disbursementB2.SMSRegistrationMessageTemplate, + WalletID: walletB.ID, + Asset: *asset2, + DisbursementReceiverRegistrationMsgTemplate: &disbursementB2.ReceiverRegistrationMessageTemplate, }, { ReceiverWallet: ReceiverWallet{ @@ -699,9 +699,9 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { PhoneNumber: receiverY.PhoneNumber, }, }, - WalletID: walletA.ID, - Asset: *asset1, - DisbursementSMSTemplate: &disbursementA1.SMSRegistrationMessageTemplate, + WalletID: walletA.ID, + Asset: *asset1, + DisbursementReceiverRegistrationMsgTemplate: &disbursementA1.ReceiverRegistrationMessageTemplate, }, { ReceiverWallet: ReceiverWallet{ @@ -712,9 +712,9 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { PhoneNumber: receiverY.PhoneNumber, }, }, - WalletID: walletA.ID, - Asset: *asset2, - DisbursementSMSTemplate: &disbursementA2.SMSRegistrationMessageTemplate, + WalletID: walletA.ID, + Asset: *asset2, + DisbursementReceiverRegistrationMsgTemplate: &disbursementA2.ReceiverRegistrationMessageTemplate, }, { ReceiverWallet: ReceiverWallet{ @@ -725,9 +725,9 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { PhoneNumber: receiverY.PhoneNumber, }, }, - WalletID: walletB.ID, - Asset: *asset1, - DisbursementSMSTemplate: &disbursementB1.SMSRegistrationMessageTemplate, + WalletID: walletB.ID, + Asset: *asset1, + DisbursementReceiverRegistrationMsgTemplate: &disbursementB1.ReceiverRegistrationMessageTemplate, }, { ReceiverWallet: ReceiverWallet{ @@ -738,9 +738,9 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) { PhoneNumber: receiverY.PhoneNumber, }, }, - WalletID: walletB.ID, - Asset: *asset2, - DisbursementSMSTemplate: &disbursementB2.SMSRegistrationMessageTemplate, + WalletID: walletB.ID, + Asset: *asset2, + DisbursementReceiverRegistrationMsgTemplate: &disbursementB2.ReceiverRegistrationMessageTemplate, }, } diff --git a/internal/data/disbursement_instructions.go b/internal/data/disbursement_instructions.go index 94e8247f5..29cf37ec6 100644 --- a/internal/data/disbursement_instructions.go +++ b/internal/data/disbursement_instructions.go @@ -298,7 +298,7 @@ func (di DisbursementInstructionModel) processReceiverWallets(ctx context.Contex } receiverIDToReceiverWalletIDMap[receiverID] = rwID } else { - _, retryErr := di.receiverWalletModel.RetryInvitationSMS(ctx, dbTx, receiverWalletID) + _, retryErr := di.receiverWalletModel.RetryInvitationMessage(ctx, dbTx, receiverWalletID) if retryErr != nil { if !errors.Is(retryErr, ErrRecordNotFound) { return nil, fmt.Errorf("retrying invitation: %w", retryErr) diff --git a/internal/data/disbursements.go b/internal/data/disbursements.go index 59b85dbbc..188662c66 100644 --- a/internal/data/disbursements.go +++ b/internal/data/disbursements.go @@ -17,19 +17,19 @@ 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"` - VerificationField VerificationType `json:"verification_field,omitempty" db:"verification_field"` - StatusHistory DisbursementStatusHistory `json:"status_history,omitempty" db:"status_history"` - SMSRegistrationMessageTemplate string `json:"sms_registration_message_template" db:"sms_registration_message_template"` - FileName string `json:"file_name,omitempty" db:"file_name"` - FileContent []byte `json:"-" db:"file_content"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + 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"` + VerificationField VerificationType `json:"verification_field,omitempty" db:"verification_field"` + StatusHistory DisbursementStatusHistory `json:"status_history,omitempty" db:"status_history"` + ReceiverRegistrationMessageTemplate string `json:"receiver_registration_message_template" db:"receiver_registration_message_template"` + FileName string `json:"file_name,omitempty" db:"file_name"` + FileContent []byte `json:"-" db:"file_content"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` *DisbursementStats } @@ -71,7 +71,7 @@ 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, sms_registration_message_template) + disbursements (name, status, status_history, wallet_id, asset_id, country_code, verification_field, receiver_registration_message_template) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id @@ -85,7 +85,7 @@ func (d *DisbursementModel) Insert(ctx context.Context, disbursement *Disburseme disbursement.Asset.ID, disbursement.Country.Code, disbursement.VerificationField, - disbursement.SMSRegistrationMessageTemplate, + disbursement.ReceiverRegistrationMessageTemplate, ) if err != nil { // check if the error is a duplicate key error @@ -127,7 +127,7 @@ func (d *DisbursementModel) Get(ctx context.Context, sqlExec db.SQLExecuter, id d.created_at, d.updated_at, d.verification_field, - COALESCE(d.sms_registration_message_template, '') as sms_registration_message_template, + COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template, w.id as "wallet.id", w.name as "wallet.name", w.homepage as "wallet.homepage", @@ -179,7 +179,7 @@ func (d *DisbursementModel) GetByName(ctx context.Context, sqlExec db.SQLExecute d.created_at, d.updated_at, d.verification_field, - COALESCE(d.sms_registration_message_template, '') as sms_registration_message_template, + COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template, w.id as "wallet.id", w.name as "wallet.name", w.homepage as "wallet.homepage", @@ -320,7 +320,7 @@ func (d *DisbursementModel) GetAll(ctx context.Context, sqlExec db.SQLExecuter, d.created_at, d.updated_at, d.verification_field, - COALESCE(d.sms_registration_message_template, '') as sms_registration_message_template, + COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template, COALESCE(d.file_name, '') as file_name, w.id as "wallet.id", w.name as "wallet.name", diff --git a/internal/data/disbursements_test.go b/internal/data/disbursements_test.go index 222f2a437..a267c45c7 100644 --- a/internal/data/disbursements_test.go +++ b/internal/data/disbursements_test.go @@ -40,11 +40,11 @@ func Test_DisbursementModelInsert(t *testing.T) { UserID: "user1", }, }, - Asset: asset, - Country: country, - Wallet: wallet, - VerificationField: VerificationTypeDateOfBirth, - SMSRegistrationMessageTemplate: smsTemplate, + Asset: asset, + Country: country, + Wallet: wallet, + VerificationField: VerificationTypeDateOfBirth, + ReceiverRegistrationMessageTemplate: smsTemplate, } t.Run("returns error when disbursement already exists", func(t *testing.T) { @@ -69,7 +69,7 @@ func Test_DisbursementModelInsert(t *testing.T) { assert.Equal(t, asset, actual.Asset) assert.Equal(t, country, actual.Country) assert.Equal(t, wallet, actual.Wallet) - assert.Equal(t, smsTemplate, actual.SMSRegistrationMessageTemplate) + assert.Equal(t, smsTemplate, 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) diff --git a/internal/data/organizations.go b/internal/data/organizations.go index 35f0a477a..ba56a9e7c 100644 --- a/internal/data/organizations.go +++ b/internal/data/organizations.go @@ -25,20 +25,20 @@ import ( ) const ( - DefaultSMSRegistrationMessageTemplate = "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register." - DefaultOTPMessageTemplate = "{{.OTP}} is your {{.OrganizationName}} phone verification code." + DefaultReceiverRegistrationMessageTemplate = "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register." + DefaultOTPMessageTemplate = "{{.OTP}} is your {{.OrganizationName}} phone verification code." ) type Organization struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` TimezoneUTCOffset string `json:"timezone_utc_offset" db:"timezone_utc_offset"` - // SMSResendInterval is the time period that SDP will wait to resend the invitation SMS to the receivers that aren't registered. - // If it's nil means resending the invitation SMS is deactivated. - SMSResendInterval *int64 `json:"sms_resend_interval" db:"sms_resend_interval"` + // ReceiverInvitationResendInterval is the time period that SDP will wait to resend the invitation to the receivers that aren't registered. + // If it's nil means resending the invitation is deactivated. + ReceiverInvitationResendIntervalDays *int64 `json:"receiver_invitation_resend_interval_days" db:"receiver_invitation_resend_interval_days"` // PaymentCancellationPeriodDays is the number of days for a ready payment to be automatically cancelled. - PaymentCancellationPeriodDays *int64 `json:"payment_cancellation_period_days" db:"payment_cancellation_period_days"` - SMSRegistrationMessageTemplate string `json:"sms_registration_message_template" db:"sms_registration_message_template"` + PaymentCancellationPeriodDays *int64 `json:"payment_cancellation_period_days" db:"payment_cancellation_period_days"` + ReceiverRegistrationMessageTemplate string `json:"receiver_registration_message_template" db:"receiver_registration_message_template"` // OTPMessageTemplate is the message template to send the OTP code to the receivers validates their identity when registering their wallets. // The message may have the template values {{.OTP}} and {{.OrganizationName}}, it will be parsed and the values injected when executing the template. // When the {{.OTP}} is not found in the message, it's added at the beginning of the message. @@ -54,17 +54,17 @@ type Organization struct { } type OrganizationUpdate struct { - Name string `json:",omitempty"` - Logo []byte `json:",omitempty"` - TimezoneUTCOffset string `json:",omitempty"` - IsApprovalRequired *bool `json:",omitempty"` - SMSResendInterval *int64 `json:",omitempty"` - PaymentCancellationPeriodDays *int64 `json:",omitempty"` + Name string `json:",omitempty"` + Logo []byte `json:",omitempty"` + TimezoneUTCOffset string `json:",omitempty"` + IsApprovalRequired *bool `json:",omitempty"` + ReceiverInvitationResendIntervalDays *int64 `json:",omitempty"` + PaymentCancellationPeriodDays *int64 `json:",omitempty"` // Using pointers to accept empty strings - SMSRegistrationMessageTemplate *string `json:",omitempty"` - OTPMessageTemplate *string `json:",omitempty"` - PrivacyPolicyLink *string `json:",omitempty"` + ReceiverRegistrationMessageTemplate *string `json:",omitempty"` + OTPMessageTemplate *string `json:",omitempty"` + PrivacyPolicyLink *string `json:",omitempty"` } type LogoType string @@ -93,7 +93,7 @@ func (lt LogoType) ToHTTPContentType() string { func (ou *OrganizationUpdate) validate() error { if ou.areAllFieldsEmpty() { - return fmt.Errorf("name, timezone UTC offset, approval workflow flag, SMS Resend Interval, SMS invite template, OTP message template, privacy policy link or logo is required") + return fmt.Errorf("name, timezone UTC offset, approval workflow flag, Receiver invitation resend interval, Receiver registration invite template, OTP message template, privacy policy link or logo is required") } if len(ou.Logo) > 0 { @@ -122,15 +122,15 @@ func (ou *OrganizationUpdate) validate() error { } func (ou *OrganizationUpdate) areAllFieldsEmpty() bool { - return (ou.Name == "" && + return ou.Name == "" && len(ou.Logo) == 0 && ou.TimezoneUTCOffset == "" && ou.IsApprovalRequired == nil && - ou.SMSRegistrationMessageTemplate == nil && + ou.ReceiverRegistrationMessageTemplate == nil && ou.OTPMessageTemplate == nil && - ou.SMSResendInterval == nil && + ou.ReceiverInvitationResendIntervalDays == nil && ou.PaymentCancellationPeriodDays == nil && - ou.PrivacyPolicyLink == nil) + ou.PrivacyPolicyLink == nil } type OrganizationModel struct { @@ -197,13 +197,13 @@ func (om *OrganizationModel) Update(ctx context.Context, ou *OrganizationUpdate) args = append(args, *ou.IsApprovalRequired) } - if ou.SMSRegistrationMessageTemplate != nil { - if *ou.SMSRegistrationMessageTemplate != "" { - fields = append(fields, "sms_registration_message_template = ?") - args = append(args, *ou.SMSRegistrationMessageTemplate) + if ou.ReceiverRegistrationMessageTemplate != nil { + if *ou.ReceiverRegistrationMessageTemplate != "" { + fields = append(fields, "receiver_registration_message_template = ?") + args = append(args, *ou.ReceiverRegistrationMessageTemplate) } else { // When empty value is passed by parameter we set the DEFAULT value for the column. - fields = append(fields, "sms_registration_message_template = DEFAULT") + fields = append(fields, "receiver_registration_message_template = DEFAULT") } } @@ -227,13 +227,13 @@ func (om *OrganizationModel) Update(ctx context.Context, ou *OrganizationUpdate) } } - if ou.SMSResendInterval != nil { - if *ou.SMSResendInterval > 0 { - fields = append(fields, "sms_resend_interval = ?") - args = append(args, *ou.SMSResendInterval) + if ou.ReceiverInvitationResendIntervalDays != nil { + if *ou.ReceiverInvitationResendIntervalDays > 0 { + fields = append(fields, "receiver_invitation_resend_interval_days = ?") + args = append(args, *ou.ReceiverInvitationResendIntervalDays) } else { // When 0 (zero) is passed by parameter we set it as NULL. - fields = append(fields, "sms_resend_interval = NULL") + fields = append(fields, "receiver_invitation_resend_interval_days = NULL") } } diff --git a/internal/data/organizations_test.go b/internal/data/organizations_test.go index 2f0124ecc..636ad53f9 100644 --- a/internal/data/organizations_test.go +++ b/internal/data/organizations_test.go @@ -30,7 +30,7 @@ func Test_Organizations_DatabaseTriggers(t *testing.T) { t.Run("SQL query will trigger an error if you try to have more than one organization", func(t *testing.T) { q := ` INSERT INTO organizations ( - name, timezone_utc_offset, sms_registration_message_template + name, timezone_utc_offset, receiver_registration_message_template ) VALUES ( 'Test name', '+00:00', 'Test template {{.OrganizationName}} {{.RegistrationLink}}.' @@ -46,8 +46,8 @@ func Test_Organizations_DatabaseTriggers(t *testing.T) { require.EqualError(t, err, "pq: organizations can must contain exactly one row") }) - t.Run("updating sms_registration_message_template with the tags {{.OrganizationName}} and {{.RegistrationLink}} will succeed ๐ŸŽ‰", func(t *testing.T) { - q := "UPDATE organizations SET sms_registration_message_template = 'TAG1: {{.OrganizationName}} and TAG2: {{.RegistrationLink}}.'" + t.Run("updating receiver_registration_message_template with the tags {{.OrganizationName}} and {{.RegistrationLink}} will succeed ๐ŸŽ‰", func(t *testing.T) { + q := "UPDATE organizations SET receiver_registration_message_template = 'TAG1: {{.OrganizationName}} and TAG2: {{.RegistrationLink}}.'" _, err := dbConnectionPool.ExecContext(ctx, q) require.NoError(t, err) }) @@ -71,7 +71,7 @@ func Test_Organizations_Get(t *testing.T) { assert.Len(t, gotOrganization.ID, 36) assert.Equal(t, "MyCustomAid", gotOrganization.Name) assert.Equal(t, "+00:00", gotOrganization.TimezoneUTCOffset) - assert.Equal(t, "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register.", gotOrganization.SMSRegistrationMessageTemplate) + assert.Equal(t, "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register.", gotOrganization.ReceiverRegistrationMessageTemplate) assert.NotEmpty(t, gotOrganization.CreatedAt) assert.NotEmpty(t, gotOrganization.UpdatedAt) assert.False(t, gotOrganization.IsApprovalRequired) @@ -83,7 +83,7 @@ func Test_Organizations_Get(t *testing.T) { func Test_OrganizationUpdate_validate(t *testing.T) { ou := &OrganizationUpdate{} err := ou.validate() - assert.EqualError(t, err, "name, timezone UTC offset, approval workflow flag, SMS Resend Interval, SMS invite template, OTP message template, privacy policy link or logo is required") + assert.EqualError(t, err, "name, timezone UTC offset, approval workflow flag, Receiver invitation resend interval, Receiver registration invite template, OTP message template, privacy policy link or logo is required") ou.Name = "My Org Name" err = ou.validate() @@ -203,7 +203,7 @@ func Test_Organizations_Update(t *testing.T) { ou := &OrganizationUpdate{} err := organizationModel.Update(ctx, ou) - assert.EqualError(t, err, "invalid organization update: name, timezone UTC offset, approval workflow flag, SMS Resend Interval, SMS invite template, OTP message template, privacy policy link or logo is required") + assert.EqualError(t, err, "invalid organization update: name, timezone UTC offset, approval workflow flag, Receiver invitation resend interval, Receiver registration invite template, OTP message template, privacy policy link or logo is required") }) t.Run("updates only organization's name successfully", func(t *testing.T) { @@ -324,41 +324,41 @@ func Test_Organizations_Update(t *testing.T) { assert.Equal(t, ou.Logo, o.Logo) }) - t.Run("updates the organization's SMSRegistrationMessageTemplate", func(t *testing.T) { + t.Run("updates the organization's ReceiverRegistrationMessageTemplate", func(t *testing.T) { defer resetOrganizationInfo(t, ctx, dbConnectionPool) defaultMessage := "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register." o, err := organizationModel.Get(ctx) require.NoError(t, err) - assert.Equal(t, defaultMessage, o.SMSRegistrationMessageTemplate) + assert.Equal(t, defaultMessage, o.ReceiverRegistrationMessageTemplate) // Setting custom message m := "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹" - ou := &OrganizationUpdate{SMSRegistrationMessageTemplate: &m} + ou := &OrganizationUpdate{ReceiverRegistrationMessageTemplate: &m} err = organizationModel.Update(ctx, ou) require.NoError(t, err) o, err = organizationModel.Get(ctx) require.NoError(t, err) - assert.Equal(t, m, o.SMSRegistrationMessageTemplate) + assert.Equal(t, m, o.ReceiverRegistrationMessageTemplate) // Don't update the message - err = organizationModel.Update(ctx, &OrganizationUpdate{Name: "My Org Name", SMSRegistrationMessageTemplate: nil}) + err = organizationModel.Update(ctx, &OrganizationUpdate{Name: "My Org Name", ReceiverRegistrationMessageTemplate: nil}) require.NoError(t, err) o, err = organizationModel.Get(ctx) require.NoError(t, err) - assert.Equal(t, m, o.SMSRegistrationMessageTemplate) + assert.Equal(t, m, o.ReceiverRegistrationMessageTemplate) // Back to the default value - ou.SMSRegistrationMessageTemplate = new(string) + ou.ReceiverRegistrationMessageTemplate = new(string) err = organizationModel.Update(ctx, ou) require.NoError(t, err) o, err = organizationModel.Get(ctx) require.NoError(t, err) - assert.Equal(t, defaultMessage, o.SMSRegistrationMessageTemplate) + assert.Equal(t, defaultMessage, o.ReceiverRegistrationMessageTemplate) }) t.Run("updates the organization's OTPMessageTemplate", func(t *testing.T) { @@ -398,29 +398,29 @@ func Test_Organizations_Update(t *testing.T) { assert.Equal(t, defaultMessage, o.OTPMessageTemplate) }) - t.Run("updates the organization's SMSResendInterval", func(t *testing.T) { + t.Run("updates the organization's ReceiverInvitationResendIntervalDays", func(t *testing.T) { defer resetOrganizationInfo(t, ctx, dbConnectionPool) o, err := organizationModel.Get(ctx) require.NoError(t, err) - assert.Nil(t, o.SMSResendInterval) + assert.Nil(t, o.ReceiverInvitationResendIntervalDays) var smsResendInterval int64 = 2 - err = organizationModel.Update(ctx, &OrganizationUpdate{SMSResendInterval: &smsResendInterval}) + err = organizationModel.Update(ctx, &OrganizationUpdate{ReceiverInvitationResendIntervalDays: &smsResendInterval}) require.NoError(t, err) o, err = organizationModel.Get(ctx) require.NoError(t, err) - assert.Equal(t, smsResendInterval, *o.SMSResendInterval) + assert.Equal(t, smsResendInterval, *o.ReceiverInvitationResendIntervalDays) // Set it as null smsResendInterval = 0 - err = organizationModel.Update(ctx, &OrganizationUpdate{SMSResendInterval: &smsResendInterval}) + err = organizationModel.Update(ctx, &OrganizationUpdate{ReceiverInvitationResendIntervalDays: &smsResendInterval}) require.NoError(t, err) o, err = organizationModel.Get(ctx) require.NoError(t, err) - assert.Nil(t, o.SMSResendInterval) + assert.Nil(t, o.ReceiverInvitationResendIntervalDays) }) t.Run("updates the organization's PaymentCancellationPeriod", func(t *testing.T) { @@ -530,7 +530,7 @@ func resetOrganizationInfo(t *testing.T, ctx context.Context, dbConnectionPool d organizations SET name = 'MyCustomAid', logo = NULL, timezone_utc_offset = '+00:00', - sms_registration_message_template = DEFAULT, otp_message_template = DEFAULT, message_channel_priority = '{"SMS", "EMAIL"}'` + receiver_registration_message_template = DEFAULT, otp_message_template = DEFAULT, message_channel_priority = '{"SMS", "EMAIL"}'` _, err := dbConnectionPool.ExecContext(ctx, q) require.NoError(t, err) } diff --git a/internal/data/receivers_wallet.go b/internal/data/receivers_wallet.go index 947387ac3..899da1172 100644 --- a/internal/data/receivers_wallet.go +++ b/internal/data/receivers_wallet.go @@ -92,9 +92,9 @@ type ReceiverWalletStats struct { CanceledPayments string `json:"canceled_payments,omitempty" db:"canceled_payments"` RemainingPayments string `json:"remaining_payments,omitempty" db:"remaining_payments"` ReceivedAmounts ReceivedAmounts `json:"received_amounts,omitempty" db:"received_amounts"` - // TotalInvitationSMSResentAttempts holds how many times were resent the Invitation SMS to the receiver + // TotalInvitationResentAttempts holds how many times were resent the Invitation SMS to the receiver // since the last invitation has been sent. - TotalInvitationSMSResentAttempts int64 `json:"-" db:"total_invitation_sms_resent_attempts"` + TotalInvitationResentAttempts int64 `json:"-" db:"total_invitation_sms_resent_attempts"` } type ReceiverWalletModel struct { @@ -554,8 +554,8 @@ func (rw *ReceiverWalletModel) UpdateAnchorPlatformTransactionSyncedAt(ctx conte return receiverWallets, nil } -// RetryInvitationSMS sets null the invitation_sent_at of a receiver wallet. -func (rw *ReceiverWalletModel) RetryInvitationSMS(ctx context.Context, sqlExec db.SQLExecuter, receiverWalletId string) (*ReceiverWallet, error) { +// RetryInvitationMessage sets null the invitation_sent_at of a receiver wallet. +func (rw *ReceiverWalletModel) RetryInvitationMessage(ctx context.Context, sqlExec db.SQLExecuter, receiverWalletId string) (*ReceiverWallet, error) { var receiverWallet ReceiverWallet query := ` UPDATE diff --git a/internal/data/receivers_wallet_test.go b/internal/data/receivers_wallet_test.go index ba229a8a6..a08875f1f 100644 --- a/internal/data/receivers_wallet_test.go +++ b/internal/data/receivers_wallet_test.go @@ -1400,7 +1400,7 @@ func Test_RetryInvitationSMS(t *testing.T) { receiverWalletModel := ReceiverWalletModel{dbConnectionPool: dbConnectionPool} t.Run("returns error when receiver wallet does not exist", func(t *testing.T) { - receiverWallet, err := receiverWalletModel.RetryInvitationSMS(ctx, dbConnectionPool, "invalid_id") + receiverWallet, err := receiverWalletModel.RetryInvitationMessage(ctx, dbConnectionPool, "invalid_id") require.Error(t, err) require.ErrorIs(t, err, ErrRecordNotFound) require.Empty(t, receiverWallet) @@ -1411,7 +1411,7 @@ func Test_RetryInvitationSMS(t *testing.T) { wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://") rw := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus) - receiverWallet, err := receiverWalletModel.RetryInvitationSMS(ctx, dbConnectionPool, rw.ID) + receiverWallet, err := receiverWalletModel.RetryInvitationMessage(ctx, dbConnectionPool, rw.ID) require.Error(t, err) require.ErrorIs(t, err, ErrRecordNotFound) require.Empty(t, receiverWallet) @@ -1422,7 +1422,7 @@ func Test_RetryInvitationSMS(t *testing.T) { wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "https://www.wallet.com", "www.wallet.com", "wallet1://") rw := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, ReadyReceiversWalletStatus) - receiverWallet, err := receiverWalletModel.RetryInvitationSMS(ctx, dbConnectionPool, rw.ID) + receiverWallet, err := receiverWalletModel.RetryInvitationMessage(ctx, dbConnectionPool, rw.ID) require.NoError(t, err) assert.Nil(t, receiverWallet.InvitationSentAt) }) diff --git a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go b/internal/events/eventhandlers/send_receiver_wallets_invitation_event_handler.go similarity index 63% rename from internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go rename to internal/events/eventhandlers/send_receiver_wallets_invitation_event_handler.go index 3fc6e29a5..37d475a89 100644 --- a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler.go +++ b/internal/events/eventhandlers/send_receiver_wallets_invitation_event_handler.go @@ -17,25 +17,25 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant" ) -type SendReceiverWalletsSMSInvitationEventHandlerOptions struct { - AdminDBConnectionPool db.DBConnectionPool - MtnDBConnectionPool db.DBConnectionPool - AnchorPlatformBaseSepURL string - MessageDispatcher message.MessageDispatcherInterface - MaxInvitationSMSResendAttempts int64 - Sep10SigningPrivateKey string - CrashTrackerClient crashtracker.CrashTrackerClient +type SendReceiverWalletsInvitationEventHandlerOptions struct { + AdminDBConnectionPool db.DBConnectionPool + MtnDBConnectionPool db.DBConnectionPool + AnchorPlatformBaseSepURL string + MessageDispatcher message.MessageDispatcherInterface + MaxInvitationResendAttempts int64 + Sep10SigningPrivateKey string + CrashTrackerClient crashtracker.CrashTrackerClient } -type SendReceiverWalletsSMSInvitationEventHandler struct { +type SendReceiverWalletsInvitationEventHandler struct { tenantManager tenant.ManagerInterface mtnDBConnectionPool db.DBConnectionPool service services.SendReceiverWalletInviteServiceInterface } -var _ events.EventHandler = new(SendReceiverWalletsSMSInvitationEventHandler) +var _ events.EventHandler = new(SendReceiverWalletsInvitationEventHandler) -func NewSendReceiverWalletsSMSInvitationEventHandler(options SendReceiverWalletsSMSInvitationEventHandlerOptions) *SendReceiverWalletsSMSInvitationEventHandler { +func NewSendReceiverWalletsInvitationEventHandler(options SendReceiverWalletsInvitationEventHandlerOptions) *SendReceiverWalletsInvitationEventHandler { tm := tenant.NewManager(tenant.WithDatabase(options.AdminDBConnectionPool)) models, err := data.NewModels(options.MtnDBConnectionPool) @@ -47,32 +47,32 @@ func NewSendReceiverWalletsSMSInvitationEventHandler(options SendReceiverWallets models, options.MessageDispatcher, options.Sep10SigningPrivateKey, - options.MaxInvitationSMSResendAttempts, + options.MaxInvitationResendAttempts, options.CrashTrackerClient, ) if err != nil { log.Fatalf("error instantiating service: %s", err.Error()) } - return &SendReceiverWalletsSMSInvitationEventHandler{ + return &SendReceiverWalletsInvitationEventHandler{ tenantManager: tm, mtnDBConnectionPool: options.MtnDBConnectionPool, service: s, } } -func (h *SendReceiverWalletsSMSInvitationEventHandler) Name() string { +func (h *SendReceiverWalletsInvitationEventHandler) Name() string { return utils.GetTypeName(h) } -func (h *SendReceiverWalletsSMSInvitationEventHandler) CanHandleMessage(ctx context.Context, message *events.Message) bool { +func (h *SendReceiverWalletsInvitationEventHandler) CanHandleMessage(ctx context.Context, message *events.Message) bool { return message.Topic == events.ReceiverWalletNewInvitationTopic } -func (h *SendReceiverWalletsSMSInvitationEventHandler) Handle(ctx context.Context, message *events.Message) error { - receiverWalletInvitationData, err := utils.ConvertType[any, []schemas.EventReceiverWalletSMSInvitationData](message.Data) +func (h *SendReceiverWalletsInvitationEventHandler) Handle(ctx context.Context, message *events.Message) error { + receiverWalletInvitationData, err := utils.ConvertType[any, []schemas.EventReceiverWalletInvitationData](message.Data) if err != nil { - return fmt.Errorf("could not convert message data to %T: %w", []schemas.EventReceiverWalletSMSInvitationData{}, err) + return fmt.Errorf("could not convert message data to %T: %w", []schemas.EventReceiverWalletInvitationData{}, err) } t, err := h.tenantManager.GetTenantByID(ctx, message.TenantID) diff --git a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler_test.go b/internal/events/eventhandlers/send_receiver_wallets_invitation_event_handler_test.go similarity index 90% rename from internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler_test.go rename to internal/events/eventhandlers/send_receiver_wallets_invitation_event_handler_test.go index 577d16fe7..89d10f906 100644 --- a/internal/events/eventhandlers/send_receiver_wallets_sms_invitation_event_handler_test.go +++ b/internal/events/eventhandlers/send_receiver_wallets_invitation_event_handler_test.go @@ -31,7 +31,7 @@ func Test_SendReceiverWalletsSMSInvitationEventHandler_Handle(t *testing.T) { service := servicesMocks.MockSendReceiverWalletInviteService{} - handler := SendReceiverWalletsSMSInvitationEventHandler{ + handler := SendReceiverWalletsInvitationEventHandler{ tenantManager: tenantManager, mtnDBConnectionPool: mtnDBConnectionPool, service: &service, @@ -40,13 +40,13 @@ func Test_SendReceiverWalletsSMSInvitationEventHandler_Handle(t *testing.T) { ctx := context.Background() t.Run("logs and report error when message Data is invalid", func(t *testing.T) { handleErr := handler.Handle(ctx, &events.Message{Data: "invalid"}) - assert.ErrorContains(t, handleErr, "could not convert message data to []schemas.EventReceiverWalletSMSInvitationData") + assert.ErrorContains(t, handleErr, "could not convert message data to []schemas.EventReceiverWalletInvitationData") }) t.Run("logs and report error when fails getting tenant by ID", func(t *testing.T) { handleErr := handler.Handle(ctx, &events.Message{ TenantID: "tenant-id", - Data: []schemas.EventReceiverWalletSMSInvitationData{ + Data: []schemas.EventReceiverWalletInvitationData{ {ReceiverWalletID: "rw-id-1"}, {ReceiverWalletID: "rw-id-2"}, }, @@ -60,7 +60,7 @@ func Test_SendReceiverWalletsSMSInvitationEventHandler_Handle(t *testing.T) { tnt, err := tenantManager.AddTenant(ctx, "myorg1") require.NoError(t, err) - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ {ReceiverWalletID: "rw-id-1"}, {ReceiverWalletID: "rw-id-2"}, } @@ -85,7 +85,7 @@ func Test_SendReceiverWalletsSMSInvitationEventHandler_Handle(t *testing.T) { tnt, err := tenantManager.AddTenant(ctx, "myorg1") require.NoError(t, err) - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ {ReceiverWalletID: "rw-id-1"}, {ReceiverWalletID: "rw-id-2"}, } @@ -109,7 +109,7 @@ func Test_SendReceiverWalletsSMSInvitationEventHandler_Handle(t *testing.T) { func Test_SendReceiverWalletsSMSInvitationEventHandler_CanHandleMessage(t *testing.T) { ctx := context.Background() - handler := SendReceiverWalletsSMSInvitationEventHandler{} + handler := SendReceiverWalletsInvitationEventHandler{} assert.False(t, handler.CanHandleMessage(ctx, &events.Message{Topic: "some-topic"})) assert.True(t, handler.CanHandleMessage(ctx, &events.Message{Topic: events.ReceiverWalletNewInvitationTopic})) diff --git a/internal/events/handler.go b/internal/events/handler.go index c704e048f..ad596f088 100644 --- a/internal/events/handler.go +++ b/internal/events/handler.go @@ -18,8 +18,8 @@ const ( // Type Names const ( - RetryReceiverWalletSMSInvitationType = "retry-receiver-wallet-sms-invitation" - BatchReceiverWalletSMSInvitationType = "batch-receiver-wallet-sms-invitation" + RetryReceiverWalletInvitationType = "retry-receiver-wallet-sms-invitation" + BatchReceiverWalletInvitationType = "batch-receiver-wallet-sms-invitation" PaymentCompletedSuccessType = "payment-completed-success" PaymentCompletedErrorType = "payment-completed-error" PaymentReadyToPayDisbursementStarted = "payment-ready-to-pay-disbursement-started" diff --git a/internal/events/schemas/schemas.go b/internal/events/schemas/schemas.go index bba41123b..6ed8d361f 100644 --- a/internal/events/schemas/schemas.go +++ b/internal/events/schemas/schemas.go @@ -2,7 +2,7 @@ package schemas import "time" -type EventReceiverWalletSMSInvitationData struct { +type EventReceiverWalletInvitationData struct { ReceiverWalletID string `json:"receiver_wallet_id"` } diff --git a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go index 44e9fb8cf..160be9342 100644 --- a/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go +++ b/internal/scheduler/jobs/send_receiver_wallets_sms_invitation_job.go @@ -14,64 +14,64 @@ import ( ) const ( - sendReceiverWalletsSMSInvitationJobName = "send_receiver_wallets_sms_invitation_job" + sendReceiverWalletsInvitationJobName = "send_receiver_wallets_invitation_job" ) -type SendReceiverWalletsSMSInvitationJobOptions struct { - Models *data.Models - MessageDispatcher message.MessageDispatcherInterface - MaxInvitationSMSResendAttempts int64 - Sep10SigningPrivateKey string - CrashTrackerClient crashtracker.CrashTrackerClient - JobIntervalSeconds int +type SendReceiverWalletsInvitationJobOptions struct { + Models *data.Models + MessageDispatcher message.MessageDispatcherInterface + MaxInvitationResendAttempts int64 + Sep10SigningPrivateKey string + CrashTrackerClient crashtracker.CrashTrackerClient + JobIntervalSeconds int } -// sendReceiverWalletsSMSInvitationJob is a job that periodically sends SMS invitations to receiver wallets. -type sendReceiverWalletsSMSInvitationJob struct { +// sendReceiverWalletsInvitationJob is a job that periodically sends invitations to receiver wallets. +type sendReceiverWalletsInvitationJob struct { service *services.SendReceiverWalletInviteService jobIntervalSeconds int } -func (j sendReceiverWalletsSMSInvitationJob) GetName() string { - return sendReceiverWalletsSMSInvitationJobName +func (j sendReceiverWalletsInvitationJob) GetName() string { + return sendReceiverWalletsInvitationJobName } -func (j sendReceiverWalletsSMSInvitationJob) GetInterval() time.Duration { +func (j sendReceiverWalletsInvitationJob) GetInterval() time.Duration { return time.Duration(j.jobIntervalSeconds) * time.Second } -func (j sendReceiverWalletsSMSInvitationJob) IsJobMultiTenant() bool { +func (j sendReceiverWalletsInvitationJob) IsJobMultiTenant() bool { return true } -func (j sendReceiverWalletsSMSInvitationJob) Execute(ctx context.Context) error { +func (j sendReceiverWalletsInvitationJob) Execute(ctx context.Context) error { if err := j.service.SendInvite(ctx); err != nil { - err = fmt.Errorf("error sending invitation SMS to receiver wallets: %w", err) + err = fmt.Errorf("error sending invitation to receiver wallets: %w", err) log.Ctx(ctx).Error(err) return err } return nil } -func NewSendReceiverWalletsSMSInvitationJob(options SendReceiverWalletsSMSInvitationJobOptions) Job { +func NewSendReceiverWalletsInvitationJob(options SendReceiverWalletsInvitationJobOptions) Job { if options.JobIntervalSeconds < DefaultMinimumJobIntervalSeconds { - log.Fatalf("job interval is not set for %s. Instantiation failed", sendReceiverWalletsSMSInvitationJobName) + log.Fatalf("job interval is not set for %s. Instantiation failed", sendReceiverWalletsInvitationJobName) } s, err := services.NewSendReceiverWalletInviteService( options.Models, options.MessageDispatcher, options.Sep10SigningPrivateKey, - options.MaxInvitationSMSResendAttempts, + options.MaxInvitationResendAttempts, options.CrashTrackerClient, ) if err != nil { log.Fatalf("error instantiating service: %s", err.Error()) } - return &sendReceiverWalletsSMSInvitationJob{ + return &sendReceiverWalletsInvitationJob{ service: s, jobIntervalSeconds: options.JobIntervalSeconds, } } -var _ Job = (*sendReceiverWalletsSMSInvitationJob)(nil) +var _ Job = (*sendReceiverWalletsInvitationJob)(nil) 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 611fb2649..10f60bb8c 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 @@ -44,12 +44,12 @@ func Test_NewSendReceiverWalletsSMSInvitationJob(t *testing.T) { t.Run("exits with status 1 when Messenger Client is missing config", func(t *testing.T) { if os.Getenv("TEST_FATAL") == "1" { - o := SendReceiverWalletsSMSInvitationJobOptions{ - Models: models, - MaxInvitationSMSResendAttempts: 3, + o := SendReceiverWalletsInvitationJobOptions{ + Models: models, + MaxInvitationResendAttempts: 3, } - NewSendReceiverWalletsSMSInvitationJob(o) + NewSendReceiverWalletsInvitationJob(o) return } @@ -69,13 +69,13 @@ func Test_NewSendReceiverWalletsSMSInvitationJob(t *testing.T) { t.Run("exits with status 1 when Base URL is empty", func(t *testing.T) { if os.Getenv("TEST_FATAL") == "1" { - o := SendReceiverWalletsSMSInvitationJobOptions{ - Models: models, - MessageDispatcher: dryRunDispatcher, - MaxInvitationSMSResendAttempts: 3, + o := SendReceiverWalletsInvitationJobOptions{ + Models: models, + MessageDispatcher: dryRunDispatcher, + MaxInvitationResendAttempts: 3, } - NewSendReceiverWalletsSMSInvitationJob(o) + NewSendReceiverWalletsInvitationJob(o) return } @@ -94,25 +94,25 @@ func Test_NewSendReceiverWalletsSMSInvitationJob(t *testing.T) { }) t.Run("returns a job instance successfully", func(t *testing.T) { - o := SendReceiverWalletsSMSInvitationJobOptions{ - Models: models, - MessageDispatcher: dryRunDispatcher, - MaxInvitationSMSResendAttempts: 3, - JobIntervalSeconds: DefaultMinimumJobIntervalSeconds, + o := SendReceiverWalletsInvitationJobOptions{ + Models: models, + MessageDispatcher: dryRunDispatcher, + MaxInvitationResendAttempts: 3, + JobIntervalSeconds: DefaultMinimumJobIntervalSeconds, } - j := NewSendReceiverWalletsSMSInvitationJob(o) + j := NewSendReceiverWalletsInvitationJob(o) assert.NotNil(t, j) }) } func Test_SendReceiverWalletsSMSInvitationJob(t *testing.T) { - j := sendReceiverWalletsSMSInvitationJob{ + j := sendReceiverWalletsInvitationJob{ jobIntervalSeconds: 5, } - assert.Equal(t, sendReceiverWalletsSMSInvitationJobName, j.GetName()) + assert.Equal(t, sendReceiverWalletsInvitationJobName, j.GetName()) assert.Equal(t, time.Duration(5)*time.Second, j.GetInterval()) } diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 790d6a312..ea74af896 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -228,9 +228,9 @@ func WithPaymentFromSubmitterJobOption(paymentJobInterval int, models *data.Mode } } -func WithSendReceiverWalletsSMSInvitationJobOption(o jobs.SendReceiverWalletsSMSInvitationJobOptions) SchedulerJobRegisterOption { +func WithSendReceiverWalletsInvitationJobOption(o jobs.SendReceiverWalletsInvitationJobOptions) SchedulerJobRegisterOption { return func(s *Scheduler) { - j := jobs.NewSendReceiverWalletsSMSInvitationJob(o) + j := jobs.NewSendReceiverWalletsInvitationJob(o) s.addJob(j) } } diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go index 0efaf102b..b5ded36e4 100644 --- a/internal/serve/httphandler/disbursement_handler.go +++ b/internal/serve/httphandler/disbursement_handler.go @@ -39,12 +39,12 @@ 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"` - SMSRegistrationMessageTemplate string `json:"sms_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"` + ReceiverRegistrationMessageTemplate string `json:"receiver_registration_message_template"` } type PatchDisbursementStatusRequest struct { @@ -120,11 +120,11 @@ func (d DisbursementHandler) PostDisbursement(w http.ResponseWriter, r *http.Req Status: data.DraftDisbursementStatus, UserID: user.ID, }}, - Wallet: wallet, - Asset: asset, - Country: country, - VerificationField: verificationField, - SMSRegistrationMessageTemplate: disbursementRequest.SMSRegistrationMessageTemplate, + Wallet: wallet, + Asset: asset, + Country: country, + VerificationField: verificationField, + ReceiverRegistrationMessageTemplate: disbursementRequest.ReceiverRegistrationMessageTemplate, } newId, err := d.Models.Disbursements.Insert(ctx, &disbursement) diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go index 9709ab483..8ec3aa2ba 100644 --- a/internal/serve/httphandler/disbursement_handler_test.go +++ b/internal/serve/httphandler/disbursement_handler_test.go @@ -261,12 +261,12 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { expectedName := "disbursement 2" requestBody, err := json.Marshal(PostDisbursementRequest{ - Name: expectedName, - CountryCode: country.Code, - AssetID: asset.ID, - WalletID: enabledWallet.ID, - VerificationField: data.VerificationTypeDateOfBirth, - SMSRegistrationMessageTemplate: smsTemplate, + Name: expectedName, + CountryCode: country.Code, + AssetID: asset.ID, + WalletID: enabledWallet.ID, + VerificationField: data.VerificationTypeDateOfBirth, + ReceiverRegistrationMessageTemplate: smsTemplate, }) require.NoError(t, err) @@ -290,7 +290,7 @@ func Test_DisbursementHandler_PostDisbursement(t *testing.T) { assert.Equal(t, 1, len(actualDisbursement.StatusHistory)) assert.Equal(t, data.DraftDisbursementStatus, actualDisbursement.StatusHistory[0].Status) assert.Equal(t, user.ID, actualDisbursement.StatusHistory[0].UserID) - assert.Equal(t, smsTemplate, actualDisbursement.SMSRegistrationMessageTemplate) + assert.Equal(t, smsTemplate, actualDisbursement.ReceiverRegistrationMessageTemplate) }) authManagerMock.AssertExpectations(t) diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go index 869ab904b..175c726f7 100644 --- a/internal/serve/httphandler/payments_handler_test.go +++ b/internal/serve/httphandler/payments_handler_test.go @@ -132,7 +132,7 @@ func Test_PaymentsHandlerGet(t *testing.T) { "status": "DRAFT", "created_at": %q, "updated_at": %q, - "sms_registration_message_template":"" + "receiver_registration_message_template":"" }, "asset": { "id": %q, diff --git a/internal/serve/httphandler/profile_handler.go b/internal/serve/httphandler/profile_handler.go index 7dbf0647d..300bd934f 100644 --- a/internal/serve/httphandler/profile_handler.go +++ b/internal/serve/httphandler/profile_handler.go @@ -50,25 +50,25 @@ type ProfileHandler struct { } type PatchOrganizationProfileRequest struct { - OrganizationName string `json:"organization_name"` - TimezoneUTCOffset string `json:"timezone_utc_offset"` - IsApprovalRequired *bool `json:"is_approval_required"` - SMSResendInterval *int64 `json:"sms_resend_interval"` - PaymentCancellationPeriodDays *int64 `json:"payment_cancellation_period_days"` - SMSRegistrationMessageTemplate *string `json:"sms_registration_message_template"` - OTPMessageTemplate *string `json:"otp_message_template"` - PrivacyPolicyLink *string `json:"privacy_policy_link"` + OrganizationName string `json:"organization_name"` + TimezoneUTCOffset string `json:"timezone_utc_offset"` + IsApprovalRequired *bool `json:"is_approval_required"` + ReceiverInvitationResendInterval *int64 `json:"receiver_invitation_resend_interval_days"` + PaymentCancellationPeriodDays *int64 `json:"payment_cancellation_period_days"` + ReceiverRegistrationMessageTemplate *string `json:"receiver_registration_message_template"` + OTPMessageTemplate *string `json:"otp_message_template"` + PrivacyPolicyLink *string `json:"privacy_policy_link"` } func (r *PatchOrganizationProfileRequest) AreAllFieldsEmpty() bool { - return (r.OrganizationName == "" && + return r.OrganizationName == "" && r.TimezoneUTCOffset == "" && r.IsApprovalRequired == nil && - r.SMSRegistrationMessageTemplate == nil && + r.ReceiverRegistrationMessageTemplate == nil && r.OTPMessageTemplate == nil && - r.SMSResendInterval == nil && + r.ReceiverInvitationResendInterval == nil && r.PaymentCancellationPeriodDays == nil && - r.PrivacyPolicyLink == nil) + r.PrivacyPolicyLink == nil } type PatchUserProfileRequest struct { @@ -160,15 +160,15 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht } organizationUpdate := data.OrganizationUpdate{ - Name: reqBody.OrganizationName, - Logo: fileContentBytes, - TimezoneUTCOffset: reqBody.TimezoneUTCOffset, - IsApprovalRequired: reqBody.IsApprovalRequired, - SMSRegistrationMessageTemplate: reqBody.SMSRegistrationMessageTemplate, - OTPMessageTemplate: reqBody.OTPMessageTemplate, - SMSResendInterval: reqBody.SMSResendInterval, - PaymentCancellationPeriodDays: reqBody.PaymentCancellationPeriodDays, - PrivacyPolicyLink: reqBody.PrivacyPolicyLink, + Name: reqBody.OrganizationName, + Logo: fileContentBytes, + TimezoneUTCOffset: reqBody.TimezoneUTCOffset, + IsApprovalRequired: reqBody.IsApprovalRequired, + ReceiverRegistrationMessageTemplate: reqBody.ReceiverRegistrationMessageTemplate, + OTPMessageTemplate: reqBody.OTPMessageTemplate, + ReceiverInvitationResendIntervalDays: reqBody.ReceiverInvitationResendInterval, + PaymentCancellationPeriodDays: reqBody.PaymentCancellationPeriodDays, + PrivacyPolicyLink: reqBody.PrivacyPolicyLink, } requestDict, err := utils.ConvertType[data.OrganizationUpdate, map[string]interface{}](organizationUpdate) if err != nil { @@ -358,28 +358,28 @@ func (h ProfileHandler) GetOrganizationInfo(rw http.ResponseWriter, req *http.Re } resp := map[string]interface{}{ - "name": org.Name, - "logo_url": lu.String(), - "distribution_account": distributionAccount, - "distribution_account_public_key": distributionAccount.Address, // TODO: deprecate `distribution_account_public_key` - "timezone_utc_offset": org.TimezoneUTCOffset, - "is_approval_required": org.IsApprovalRequired, - "sms_resend_interval": 0, - "payment_cancellation_period_days": 0, - "privacy_policy_link": org.PrivacyPolicyLink, - "message_channel_priority": org.MessageChannelPriority, + "name": org.Name, + "logo_url": lu.String(), + "distribution_account": distributionAccount, + "distribution_account_public_key": distributionAccount.Address, // TODO: deprecate `distribution_account_public_key` + "timezone_utc_offset": org.TimezoneUTCOffset, + "is_approval_required": org.IsApprovalRequired, + "receiver_invitation_resend_interval_days": 0, + "payment_cancellation_period_days": 0, + "privacy_policy_link": org.PrivacyPolicyLink, + "message_channel_priority": org.MessageChannelPriority, } - if org.SMSRegistrationMessageTemplate != data.DefaultSMSRegistrationMessageTemplate { - resp["sms_registration_message_template"] = org.SMSRegistrationMessageTemplate + if org.ReceiverRegistrationMessageTemplate != data.DefaultReceiverRegistrationMessageTemplate { + resp["receiver_registration_message_template"] = org.ReceiverRegistrationMessageTemplate } if org.OTPMessageTemplate != data.DefaultOTPMessageTemplate { resp["otp_message_template"] = org.OTPMessageTemplate } - if org.SMSResendInterval != nil { - resp["sms_resend_interval"] = *org.SMSResendInterval + if org.ReceiverInvitationResendIntervalDays != nil { + resp["receiver_invitation_resend_interval_days"] = *org.ReceiverInvitationResendIntervalDays } if org.PaymentCancellationPeriodDays != nil { diff --git a/internal/serve/httphandler/profile_handler_test.go b/internal/serve/httphandler/profile_handler_test.go index 93bd08f9f..59df26a62 100644 --- a/internal/serve/httphandler/profile_handler_test.go +++ b/internal/serve/httphandler/profile_handler_test.go @@ -76,8 +76,8 @@ func resetOrganizationInfo(t *testing.T, ctx context.Context, dbConnectionPool d organizations SET name = 'MyCustomAid', logo = NULL, timezone_utc_offset = '+00:00', - sms_registration_message_template = DEFAULT, otp_message_template = DEFAULT, - sms_resend_interval = NULL, payment_cancellation_period_days = NULL, privacy_policy_link = NULL` + receiver_registration_message_template = DEFAULT, otp_message_template = DEFAULT, + receiver_invitation_resend_interval_days = NULL, payment_cancellation_period_days = NULL, privacy_policy_link = NULL` _, err := dbConnectionPool.ExecContext(ctx, q) require.NoError(t, err) } @@ -346,25 +346,25 @@ func Test_ProfileHandler_PatchOrganizationProfile_Successful(t *testing.T) { "organization_name": "My Org Name", "otp_message_template": "Here's your OTP Code to complete your registration. MyOrg ๐Ÿ‘‹", "payment_cancellation_period_days": 2, - "sms_registration_message_template": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", - "sms_resend_interval": 2, + "receiver_registration_message_template": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", + "receiver_invitation_resend_interval_days": 2, "timezone_utc_offset": "-03:00", "privacy_policy_link": "https://example.com/privacy-policy" }` return createOrganizationProfileMultipartRequest(t, ctx, url, "logo", "logo.png", reqBody, newPNGImgBuf()) }, resultingFieldsToCompare: map[string]interface{}{ - "IsApprovalRequired": true, - "Name": "My Org Name", - "Logo": newPNGImgBuf().Bytes(), - "OTPMessageTemplate": "Here's your OTP Code to complete your registration. MyOrg ๐Ÿ‘‹", - "PaymentCancellationPeriodDays": int64(2), - "SMSRegistrationMessageTemplate": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", - "SMSResendInterval": int64(2), - "TimezoneUTCOffset": "-03:00", - "PrivacyPolicyLink": "https://example.com/privacy-policy", + "IsApprovalRequired": true, + "Name": "My Org Name", + "Logo": newPNGImgBuf().Bytes(), + "OTPMessageTemplate": "Here's your OTP Code to complete your registration. MyOrg ๐Ÿ‘‹", + "PaymentCancellationPeriodDays": int64(2), + "ReceiverRegistrationMessageTemplate": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", + "ReceiverInvitationResendIntervalDays": int64(2), + "TimezoneUTCOffset": "-03:00", + "PrivacyPolicyLink": "https://example.com/privacy-policy", }, - wantLogEntries: []string{"[PatchOrganizationProfile] - userID user-id will update the organization fields [IsApprovalRequired='true', Logo='...', Name='My Org Name', OTPMessageTemplate='Here's your OTP Code to complete your registration. MyOrg ๐Ÿ‘‹', PaymentCancellationPeriodDays='2', PrivacyPolicyLink='https://example.com/privacy-policy', SMSRegistrationMessageTemplate='My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹', SMSResendInterval='2', TimezoneUTCOffset='-03:00']"}, + wantLogEntries: []string{"[PatchOrganizationProfile] - userID user-id will update the organization fields [IsApprovalRequired='true', Logo='...', Name='My Org Name', OTPMessageTemplate='Here's your OTP Code to complete your registration. MyOrg ๐Ÿ‘‹', PaymentCancellationPeriodDays='2', PrivacyPolicyLink='https://example.com/privacy-policy', ReceiverInvitationResendIntervalDays='2', ReceiverRegistrationMessageTemplate='My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹', TimezoneUTCOffset='-03:00']"}, }, { name: "๐ŸŽ‰ successfully updates organization back to its default values", @@ -377,37 +377,37 @@ func Test_ProfileHandler_PatchOrganizationProfile_Successful(t *testing.T) { }, updateOrgInitialValuesFn: func(t *testing.T, ctx context.Context, models *data.Models) { otpMessageTemplate := "custom OTPMessageTemplate" - smsRegistrationMessageTemplate := "custom SMSRegistrationMessageTemplate" - smsResendInterval := int64(123) + receiverRegistrationMessageTemplate := "custom ReceiverRegistrationMessageTemplate" + receiverInvitationResendInterval := int64(123) paymentCancellationPeriodDays := int64(456) privacyPolicyLink := "https://example.com/privacy-policy" err := models.Organizations.Update(ctx, &data.OrganizationUpdate{ - SMSRegistrationMessageTemplate: &smsRegistrationMessageTemplate, - OTPMessageTemplate: &otpMessageTemplate, - SMSResendInterval: &smsResendInterval, - PaymentCancellationPeriodDays: &paymentCancellationPeriodDays, - PrivacyPolicyLink: &privacyPolicyLink, + ReceiverRegistrationMessageTemplate: &receiverRegistrationMessageTemplate, + OTPMessageTemplate: &otpMessageTemplate, + ReceiverInvitationResendIntervalDays: &receiverInvitationResendInterval, + PaymentCancellationPeriodDays: &paymentCancellationPeriodDays, + PrivacyPolicyLink: &privacyPolicyLink, }) require.NoError(t, err) }, getRequestFn: func(t *testing.T, ctx context.Context) *http.Request { reqBody := `{ - "sms_registration_message_template": "", + "receiver_registration_message_template": "", "otp_message_template": "", - "sms_resend_interval": 0, + "receiver_invitation_resend_interval_days": 0, "payment_cancellation_period_days": 0, "privacy_policy_link": "" }` return createOrganizationProfileMultipartRequest(t, ctx, url, "", "", reqBody, new(bytes.Buffer)) }, resultingFieldsToCompare: map[string]interface{}{ - "SMSRegistrationMessageTemplate": "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register.", - "OTPMessageTemplate": "{{.OTP}} is your {{.OrganizationName}} phone verification code.", - "SMSResendInterval": nilInt64, - "PaymentCancellationPeriodDays": nilInt64, - "PrivacyPolicyLink": nilString, + "ReceiverRegistrationMessageTemplate": "You have a payment waiting for you from the {{.OrganizationName}}. Click {{.RegistrationLink}} to register.", + "OTPMessageTemplate": "{{.OTP}} is your {{.OrganizationName}} phone verification code.", + "ReceiverInvitationResendIntervalDays": nilInt64, + "PaymentCancellationPeriodDays": nilInt64, + "PrivacyPolicyLink": nilString, }, - wantLogEntries: []string{"[PatchOrganizationProfile] - userID user-id will update the organization fields [OTPMessageTemplate='', PaymentCancellationPeriodDays='0', PrivacyPolicyLink='', SMSRegistrationMessageTemplate='', SMSResendInterval='0']"}, + wantLogEntries: []string{"[PatchOrganizationProfile] - userID user-id will update the organization fields [OTPMessageTemplate='', PaymentCancellationPeriodDays='0', PrivacyPolicyLink='', ReceiverInvitationResendIntervalDays='0', ReceiverRegistrationMessageTemplate='']"}, }, } @@ -1134,7 +1134,7 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "timezone_utc_offset": "+00:00", "is_approval_required": false, "privacy_policy_link": null, - "sms_resend_interval": 0, + "receiver_invitation_resend_interval_days": 0, "payment_cancellation_period_days": 0, "message_channel_priority": ["SMS", "EMAIL"] } @@ -1144,12 +1144,12 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { assert.JSONEq(t, wantsBody, string(respBody)) }) - t.Run("returns the sms_registration_message_template and otp_message_template when they aren't the default values", func(t *testing.T) { + t.Run("returns the receiver_registration_message_template and otp_message_template when they aren't the default values", func(t *testing.T) { ctx = context.WithValue(ctx, middleware.TokenContextKey, "mytoken") msg := "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹" err := models.Organizations.Update(ctx, &data.OrganizationUpdate{ - SMSRegistrationMessageTemplate: &msg, + ReceiverRegistrationMessageTemplate: &msg, }) require.NoError(t, err) @@ -1171,8 +1171,8 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "distribution_account_public_key": %q, "timezone_utc_offset": "+00:00", "is_approval_required":false, - "sms_registration_message_template": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", - "sms_resend_interval": 0, + "receiver_registration_message_template": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", + "receiver_invitation_resend_interval_days": 0, "payment_cancellation_period_days": 0, "privacy_policy_link": null, "message_channel_priority": ["SMS", "EMAIL"] @@ -1206,9 +1206,9 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "distribution_account_public_key": %q, "timezone_utc_offset": "+00:00", "is_approval_required":false, - "sms_registration_message_template": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", + "receiver_registration_message_template": "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹", "otp_message_template": "Here's your OTP Code to complete your registration. MyOrg ๐Ÿ‘‹", - "sms_resend_interval": 0, + "receiver_invitation_resend_interval_days": 0, "payment_cancellation_period_days": 0, "privacy_policy_link": null, "message_channel_priority": ["SMS", "EMAIL"] @@ -1219,14 +1219,14 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { assert.JSONEq(t, wantsBody, string(respBody)) }) - t.Run("returns the custom sms_resend_interval", func(t *testing.T) { + t.Run("returns the custom receiver_invitation_resend_interval_days", func(t *testing.T) { resetOrganizationInfo(t, ctx, dbConnectionPool) ctx = context.WithValue(ctx, middleware.TokenContextKey, "mytoken") - var smsResendInterval int64 = 2 + var resendInterval int64 = 2 err := models.Organizations.Update(ctx, &data.OrganizationUpdate{ - SMSResendInterval: &smsResendInterval, + ReceiverInvitationResendIntervalDays: &resendInterval, }) require.NoError(t, err) @@ -1248,7 +1248,7 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "distribution_account_public_key": %q, "timezone_utc_offset": "+00:00", "is_approval_required":false, - "sms_resend_interval": 2, + "receiver_invitation_resend_interval_days": 2, "payment_cancellation_period_days": 0, "privacy_policy_link": null, "message_channel_priority": ["SMS", "EMAIL"] @@ -1288,7 +1288,7 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "distribution_account_public_key": %q, "timezone_utc_offset": "+00:00", "is_approval_required":false, - "sms_resend_interval": 0, + "receiver_invitation_resend_interval_days": 0, "payment_cancellation_period_days": 5, "privacy_policy_link": null, "message_channel_priority": ["SMS", "EMAIL"] @@ -1328,7 +1328,7 @@ func Test_ProfileHandler_GetOrganizationInfo(t *testing.T) { "distribution_account_public_key": %q, "timezone_utc_offset": "+00:00", "is_approval_required":false, - "sms_resend_interval": 0, + "receiver_invitation_resend_interval_days": 0, "payment_cancellation_period_days": 0, "privacy_policy_link": "https://example.com/privacy-policy", "message_channel_priority": ["SMS", "EMAIL"] diff --git a/internal/serve/httphandler/receiver_wallets_handler.go b/internal/serve/httphandler/receiver_wallets_handler.go index 527ccf708..f2ed32768 100644 --- a/internal/serve/httphandler/receiver_wallets_handler.go +++ b/internal/serve/httphandler/receiver_wallets_handler.go @@ -18,7 +18,7 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant" ) -type RetryInvitationSMSResponse struct { +type RetryInvitationMessageResponse struct { ID string `json:"id"` ReceiverID string `json:"receiver_id"` WalletID string `json:"wallet_id"` @@ -39,13 +39,13 @@ func (h ReceiverWalletsHandler) RetryInvitation(rw http.ResponseWriter, req *htt var msg *events.Message receiverWallet, err := db.RunInTransactionWithResult(ctx, h.Models.DBConnectionPool, nil, func(dbTx db.DBTransaction) (*data.ReceiverWallet, error) { - receiverWallet, err := h.Models.ReceiverWallet.RetryInvitationSMS(ctx, dbTx, receiverWalletID) + receiverWallet, err := h.Models.ReceiverWallet.RetryInvitationMessage(ctx, dbTx, receiverWalletID) if err != nil { - return nil, fmt.Errorf("retrying invitation SMS for receiver wallet ID %s: %w", receiverWalletID, err) + return nil, fmt.Errorf("retrying invitation message for receiver wallet ID %s: %w", receiverWalletID, err) } - eventData := []schemas.EventReceiverWalletSMSInvitationData{{ReceiverWalletID: receiverWalletID}} - msg, err = events.NewMessage(ctx, events.ReceiverWalletNewInvitationTopic, receiverWalletID, events.RetryReceiverWalletSMSInvitationType, eventData) + eventData := []schemas.EventReceiverWalletInvitationData{{ReceiverWalletID: receiverWalletID}} + msg, err = events.NewMessage(ctx, events.ReceiverWalletNewInvitationTopic, receiverWalletID, events.RetryReceiverWalletInvitationType, eventData) if err != nil { return nil, fmt.Errorf("creating event producer message: %w", err) } @@ -77,7 +77,7 @@ func (h ReceiverWalletsHandler) RetryInvitation(rw http.ResponseWriter, req *htt } } - response := RetryInvitationSMSResponse{ + response := RetryInvitationMessageResponse{ ID: receiverWallet.ID, ReceiverID: receiverWallet.Receiver.ID, WalletID: receiverWallet.Wallet.ID, diff --git a/internal/serve/httphandler/receiver_wallets_handler_test.go b/internal/serve/httphandler/receiver_wallets_handler_test.go index 40592d814..15a556b8b 100644 --- a/internal/serve/httphandler/receiver_wallets_handler_test.go +++ b/internal/serve/httphandler/receiver_wallets_handler_test.go @@ -94,8 +94,8 @@ func Test_RetryInvitation(t *testing.T) { Topic: events.ReceiverWalletNewInvitationTopic, Key: rw.ID, TenantID: tnt.ID, - Type: events.RetryReceiverWalletSMSInvitationType, - Data: []schemas.EventReceiverWalletSMSInvitationData{ + Type: events.RetryReceiverWalletInvitationType, + Data: []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rw.ID, }, @@ -147,8 +147,8 @@ func Test_RetryInvitation(t *testing.T) { Topic: events.ReceiverWalletNewInvitationTopic, Key: rw.ID, TenantID: tnt.ID, - Type: events.RetryReceiverWalletSMSInvitationType, - Data: []schemas.EventReceiverWalletSMSInvitationData{ + Type: events.RetryReceiverWalletInvitationType, + Data: []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rw.ID, }, @@ -220,8 +220,8 @@ func Test_RetryInvitation(t *testing.T) { Topic: events.ReceiverWalletNewInvitationTopic, Key: rw.ID, TenantID: tnt.ID, - Type: events.RetryReceiverWalletSMSInvitationType, - Data: []schemas.EventReceiverWalletSMSInvitationData{ + Type: events.RetryReceiverWalletInvitationType, + Data: []schemas.EventReceiverWalletInvitationData{ {ReceiverWalletID: rw.ID}, }, } diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 06203ca93..972bbf7d2 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -89,7 +89,7 @@ type ServeOptions struct { DistributionAccountService services.DistributionAccountServiceInterface DistAccEncryptionPassphrase string EventProducer events.Producer - MaxInvitationSMSResendAttempts int + MaxInvitationResendAttempts int SingleTenantMode bool CircleService circle.ServiceInterface } diff --git a/internal/services/disbursement_management_service.go b/internal/services/disbursement_management_service.go index ca3ffc498..9df0adf9d 100644 --- a/internal/services/disbursement_management_service.go +++ b/internal/services/disbursement_management_service.go @@ -266,12 +266,12 @@ func (s *DisbursementManagementService) StartDisbursement(ctx context.Context, d } if len(receiverWallets) != 0 { - eventData := make([]schemas.EventReceiverWalletSMSInvitationData, 0, len(receiverWallets)) + eventData := make([]schemas.EventReceiverWalletInvitationData, 0, len(receiverWallets)) for _, receiverWallet := range receiverWallets { - eventData = append(eventData, schemas.EventReceiverWalletSMSInvitationData{ReceiverWalletID: receiverWallet.ID}) + eventData = append(eventData, schemas.EventReceiverWalletInvitationData{ReceiverWalletID: receiverWallet.ID}) } - sendInviteMsg, msgErr := events.NewMessage(ctx, events.ReceiverWalletNewInvitationTopic, disbursement.ID, events.BatchReceiverWalletSMSInvitationType, eventData) + sendInviteMsg, msgErr := events.NewMessage(ctx, events.ReceiverWalletNewInvitationTopic, disbursement.ID, events.BatchReceiverWalletInvitationType, eventData) if msgErr != nil { return nil, fmt.Errorf("creating new message: %w", msgErr) } diff --git a/internal/services/disbursement_management_service_test.go b/internal/services/disbursement_management_service_test.go index ab0dfcee8..63f57b48a 100644 --- a/internal/services/disbursement_management_service_test.go +++ b/internal/services/disbursement_management_service_test.go @@ -408,13 +408,13 @@ func Test_DisbursementManagementService_StartDisbursement_success(t *testing.T) sendInviteMsg := msgs[0] assert.Equal(t, events.ReceiverWalletNewInvitationTopic, sendInviteMsg.Topic) assert.Equal(t, readyDisbursement.ID, sendInviteMsg.Key) - assert.Equal(t, events.BatchReceiverWalletSMSInvitationType, sendInviteMsg.Type) + assert.Equal(t, events.BatchReceiverWalletInvitationType, sendInviteMsg.Type) assert.Equal(t, tnt.ID, sendInviteMsg.TenantID) - eventData, ok := sendInviteMsg.Data.([]schemas.EventReceiverWalletSMSInvitationData) + eventData, ok := sendInviteMsg.Data.([]schemas.EventReceiverWalletInvitationData) require.True(t, ok) require.Len(t, eventData, 2) - wantElements := []schemas.EventReceiverWalletSMSInvitationData{ + wantElements := []schemas.EventReceiverWalletInvitationData{ {ReceiverWalletID: rwDraft.ID}, // <--- invitation for the receiver that is being included in the system for the first time {ReceiverWalletID: rwReady.ID}, // <--- invitation for the receiver that is already in the system but doesn't have a Stellar wallet yet } @@ -750,8 +750,8 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T) Topic: events.ReceiverWalletNewInvitationTopic, Key: disbursement.ID, TenantID: tnt.ID, - Type: events.BatchReceiverWalletSMSInvitationType, - Data: []schemas.EventReceiverWalletSMSInvitationData{ + Type: events.BatchReceiverWalletInvitationType, + Data: []schemas.EventReceiverWalletInvitationData{ {ReceiverWalletID: rwReady.ID}, // Receiver that can receive SMS }, }, @@ -853,8 +853,8 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T) Topic: events.ReceiverWalletNewInvitationTopic, Key: disbursement.ID, TenantID: tnt.ID, - Type: events.BatchReceiverWalletSMSInvitationType, - Data: []schemas.EventReceiverWalletSMSInvitationData{ + Type: events.BatchReceiverWalletInvitationType, + Data: []schemas.EventReceiverWalletInvitationData{ {ReceiverWalletID: rwReady.ID}, // Receiver that can receive SMS }, }, @@ -1014,8 +1014,8 @@ func Test_DisbursementManagementService_StartDisbursement_failure(t *testing.T) Topic: events.ReceiverWalletNewInvitationTopic, Key: disbursement.ID, TenantID: tnt.ID, - Type: events.BatchReceiverWalletSMSInvitationType, - Data: []schemas.EventReceiverWalletSMSInvitationData{ + Type: events.BatchReceiverWalletInvitationType, + Data: []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rwReady.ID, }, diff --git a/internal/services/mocks/send_receiver_wallets_invite_service.go b/internal/services/mocks/send_receiver_wallets_invite_service.go index 252c7ce3a..45346092f 100644 --- a/internal/services/mocks/send_receiver_wallets_invite_service.go +++ b/internal/services/mocks/send_receiver_wallets_invite_service.go @@ -12,7 +12,7 @@ type MockSendReceiverWalletInviteService struct { mock.Mock } -func (s *MockSendReceiverWalletInviteService) SendInvite(ctx context.Context, receiverWalletsReq ...schemas.EventReceiverWalletSMSInvitationData) error { +func (s *MockSendReceiverWalletInviteService) SendInvite(ctx context.Context, receiverWalletsReq ...schemas.EventReceiverWalletInvitationData) error { args := s.Called(ctx, receiverWalletsReq) return args.Error(0) } diff --git a/internal/services/send_receiver_wallets_invite_service.go b/internal/services/send_receiver_wallets_invite_service.go index 3e3c6d05e..e05b9faa2 100644 --- a/internal/services/send_receiver_wallets_invite_service.go +++ b/internal/services/send_receiver_wallets_invite_service.go @@ -23,15 +23,15 @@ import ( ) type SendReceiverWalletInviteServiceInterface interface { - SendInvite(ctx context.Context, receiverWalletInvitationData ...schemas.EventReceiverWalletSMSInvitationData) error + SendInvite(ctx context.Context, receiverWalletInvitationData ...schemas.EventReceiverWalletInvitationData) error } type SendReceiverWalletInviteService struct { - messageDispatcher message.MessageDispatcherInterface - Models *data.Models - maxInvitationSMSResendAttempts int64 - sep10SigningPrivateKey string - crashTrackerClient crashtracker.CrashTrackerClient + messageDispatcher message.MessageDispatcherInterface + Models *data.Models + maxInvitationResendAttempts int64 + sep10SigningPrivateKey string + crashTrackerClient crashtracker.CrashTrackerClient } var _ SendReceiverWalletInviteServiceInterface = new(SendReceiverWalletInviteService) @@ -49,7 +49,7 @@ func (s SendReceiverWalletInviteService) validate() error { // For instance, the Wallet Foo is in two Ready Payments, one with USDC and the other with EUROC. // So the receiver who has a Stellar Address pending registration (status:READY) in this wallet will receive both invites for USDC and EUROC. // This would not impact the user receiving both token amounts. It's only for the registration process. -func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receiverWalletInvitationData ...schemas.EventReceiverWalletSMSInvitationData) error { +func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receiverWalletInvitationData ...schemas.EventReceiverWalletInvitationData) error { if s.Models == nil { return fmt.Errorf("SendReceiverWalletInviteService.Models cannot be nil") } @@ -62,26 +62,26 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive return fmt.Errorf("tenant base URL cannot be nil for tenant %s", currentTenant.ID) } - // Get the organization entry to get the Org name and SMSRegistrationMessageTemplate + // Get the organization entry to get the Org name and ReceiverRegistrationMessageTemplate organization, err := s.Models.Organizations.Get(ctx) if err != nil { return fmt.Errorf("error getting organization: %w", err) } // Debug purposes - if organization.SMSResendInterval == nil { - log.Ctx(ctx).Debug("automatic resend invitation SMS is deactivated. Set a valid value to the organization's sms_resend_interval to activate it.") + if organization.ReceiverInvitationResendIntervalDays == nil { + log.Ctx(ctx).Debug("automatic resend invitation is deactivated. Set a valid value to the organization's receiver_invitation_resend_interval_days to activate it.") } - orgSMSRegistrationMessageTemplate := organization.SMSRegistrationMessageTemplate - if !strings.Contains(orgSMSRegistrationMessageTemplate, "{{.RegistrationLink}}") { - orgSMSRegistrationMessageTemplate = fmt.Sprintf("%s {{.RegistrationLink}}", strings.TrimSpace(orgSMSRegistrationMessageTemplate)) + orgReceiverRegistrationMessageTemplate := organization.ReceiverRegistrationMessageTemplate + if !strings.Contains(orgReceiverRegistrationMessageTemplate, "{{.RegistrationLink}}") { + orgReceiverRegistrationMessageTemplate = fmt.Sprintf("%s {{.RegistrationLink}}", strings.TrimSpace(orgReceiverRegistrationMessageTemplate)) } // Execute the template early so we avoid hitting the database to query the other info - msgTemplate, err := template.New("").Parse(orgSMSRegistrationMessageTemplate) + msgTemplate, err := template.New("").Parse(orgReceiverRegistrationMessageTemplate) if err != nil { - return fmt.Errorf("error parsing organization SMS registration message template: %w", err) + return fmt.Errorf("error parsing organization receiver registration message template: %w", err) } wallets, err := s.Models.Wallets.GetAll(ctx) @@ -108,7 +108,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive receiverWalletIDs := []string{} // TODO: improve this code adding go routines for _, rwa := range receiverWalletsAsset { - if !s.shouldSendInvitationSMS(ctx, organization, &rwa) { + if !s.shouldSendInvitation(ctx, organization, &rwa) { continue } @@ -131,15 +131,15 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive continue } - disbursementSMSRegistrationMessageTemplate := rwa.DisbursementSMSTemplate - if disbursementSMSRegistrationMessageTemplate != nil && *disbursementSMSRegistrationMessageTemplate != "" { - if !strings.Contains(*disbursementSMSRegistrationMessageTemplate, "{{.RegistrationLink}}") { - *disbursementSMSRegistrationMessageTemplate = fmt.Sprintf("%s {{.RegistrationLink}}", strings.TrimSpace(*disbursementSMSRegistrationMessageTemplate)) + disbursementReceiverRegistrationMessageTemplate := rwa.DisbursementReceiverRegistrationMsgTemplate + if disbursementReceiverRegistrationMessageTemplate != nil && *disbursementReceiverRegistrationMessageTemplate != "" { + if !strings.Contains(*disbursementReceiverRegistrationMessageTemplate, "{{.RegistrationLink}}") { + *disbursementReceiverRegistrationMessageTemplate = fmt.Sprintf("%s {{.RegistrationLink}}", strings.TrimSpace(*disbursementReceiverRegistrationMessageTemplate)) } - msgTemplate, err = template.New("").Parse(*disbursementSMSRegistrationMessageTemplate) + msgTemplate, err = template.New("").Parse(*disbursementReceiverRegistrationMessageTemplate) if err != nil { - return fmt.Errorf("parsing disbursement SMS registration message template: %w", err) + return fmt.Errorf("parsing disbursement receiver registration message template: %w", err) } } @@ -200,8 +200,8 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive msgsToInsert = append(msgsToInsert, msgToInsert) - // We don't want to update the `invitation_sent_at` for receiver wallets that we've sent the invitation SMS - // because there's no way to calculate how many times we've resent the invitation SMS since + // We don't want to update the `invitation_sent_at` for receiver wallets for which we've already sent the invitation message + // because there's no way to calculate how many times we've resent the invitation message since // the first invitation if we update it. if rwa.ReceiverWallet.InvitationSentAt == nil && msgToInsert.Status == data.SuccessMessageStatus { receiverWalletIDs = append(receiverWalletIDs, rwa.ReceiverWallet.ID) @@ -223,7 +223,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive // resolveReceiverWalletsPendingRegistration returns the receiver wallets pending registration based on the receiverWalletInvitationData. // If the receiverWalletInvitationData is empty, it will return all receiver wallets pending registration. -func (s SendReceiverWalletInviteService) resolveReceiverWalletsPendingRegistration(ctx context.Context, receiverWalletInvitationData []schemas.EventReceiverWalletSMSInvitationData) ([]*data.ReceiverWallet, error) { +func (s SendReceiverWalletInviteService) resolveReceiverWalletsPendingRegistration(ctx context.Context, receiverWalletInvitationData []schemas.EventReceiverWalletInvitationData) ([]*data.ReceiverWallet, error) { var err error var receiverWallets []*data.ReceiverWallet if len(receiverWalletInvitationData) == 0 { @@ -244,10 +244,10 @@ func (s SendReceiverWalletInviteService) resolveReceiverWalletsPendingRegistrati return receiverWallets, err } -// shouldSendInvitationSMS returns true if we should send the invitation SMS to the receiver. It will be used to either -// send the invitation for the first time, or to resend it automatically according to the organization's SMS Resend -// Interval and the maximum number of SMS resend attempts. -func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Context, organization *data.Organization, rwa *data.ReceiverWalletAsset) bool { +// shouldSendInvitation returns true if we should send the invitation to the receiver. It will be used to either +// send the invitation for the first time, or to resend it automatically according to the organization's Resend +// Interval and the maximum number of resend attempts. +func (s SendReceiverWalletInviteService) shouldSendInvitation(ctx context.Context, organization *data.Organization, rwa *data.ReceiverWalletAsset) bool { receiver := rwa.ReceiverWallet.Receiver // TODO: SDP-1316 - add support for other contact information in this method. @@ -260,45 +260,45 @@ func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Con truncatedReceiverContact := utils.TruncateString(phoneNumber, 3) - // We've never sent a Invitation SMS + // We've never sent an Invitation message if rwa.ReceiverWallet.InvitationSentAt == nil { return true } - // If organization's SMS Resend Interval is nil and we've sent the invitation message to the receiver, we won't resend it. - if organization.SMSResendInterval == nil && rwa.ReceiverWallet.InvitationSentAt != nil { + // If organization's Receiver Invitation Resend Interval is nil and we've sent the invitation message to the receiver, we won't resend it. + if organization.ReceiverInvitationResendIntervalDays == nil && rwa.ReceiverWallet.InvitationSentAt != nil { log.Ctx(ctx).Debugf( - "the invitation message was not automatically resent to the receiver %s with phone number %s because the organization's SMS Resend Interval is nil", + "the invitation message was not automatically resent to the receiver %s with contact %s because the organization's Receiver Invitation Resend Interval is nil", receiver.ID, truncatedReceiverContact) return false } - // The organizations has a interval to automatic resend the Invitation SMS. - if organization.SMSResendInterval != nil { - // Check if the receiver wallet reached the maximum number of SMS resend attempts. - if rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationSMSResentAttempts >= s.maxInvitationSMSResendAttempts { + // The organizations defined an interval to automatically resend the receiver invitation message. + if organization.ReceiverInvitationResendIntervalDays != nil { + // Check if the receiver wallet reached the maximum number of resend attempts. + if rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationResentAttempts >= s.maxInvitationResendAttempts { log.Ctx(ctx).Debugf( - "the invitation message was not resent to the receiver because the maximum number of SMS resend attempts has been reached: Phone Number: %s - Receiver ID %s - Wallet ID %s - Total Invitation SMS resent %d - Maximum attempts %d", + "the invitation message was not resent to the receiver because the maximum number of message resend attempts has been reached: Contact: %s - Receiver ID %s - Wallet ID %s - Total Invitation resent %d - Maximum attempts %d", truncatedReceiverContact, receiver.ID, rwa.WalletID, - rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationSMSResentAttempts, - s.maxInvitationSMSResendAttempts, + rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationResentAttempts, + s.maxInvitationResendAttempts, ) return false } // Check if it's in the period to resend it. resendPeriod := time.Now(). - AddDate(0, 0, -int(*organization.SMSResendInterval*(rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationSMSResentAttempts+1))) + AddDate(0, 0, -int(*organization.ReceiverInvitationResendIntervalDays*(rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationResentAttempts+1))) if !rwa.ReceiverWallet.InvitationSentAt.Before(resendPeriod) { log.Ctx(ctx).Debugf( - "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Phone Number: %s - Receiver ID %s - Wallet ID %s - Last Invitation Sent At %s - SMS Resend Interval %d day(s)", + "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Contact: %s - Receiver ID %s - Wallet ID %s - Last Invitation Sent At %s - Receiver Invitation Resend Interval %d day(s)", truncatedReceiverContact, receiver.ID, rwa.WalletID, rwa.ReceiverWallet.InvitationSentAt.Format(time.RFC1123), - *organization.SMSResendInterval, + *organization.ReceiverInvitationResendIntervalDays, ) return false } @@ -307,13 +307,13 @@ func (s SendReceiverWalletInviteService) shouldSendInvitationSMS(ctx context.Con return true } -func NewSendReceiverWalletInviteService(models *data.Models, messageDispatcher message.MessageDispatcherInterface, sep10SigningPrivateKey string, maxInvitationSMSResendAttempts int64, crashTrackerClient crashtracker.CrashTrackerClient) (*SendReceiverWalletInviteService, error) { +func NewSendReceiverWalletInviteService(models *data.Models, messageDispatcher message.MessageDispatcherInterface, sep10SigningPrivateKey string, maxInvitationResendAttempts int64, crashTrackerClient crashtracker.CrashTrackerClient) (*SendReceiverWalletInviteService, error) { s := &SendReceiverWalletInviteService{ - messageDispatcher: messageDispatcher, - Models: models, - maxInvitationSMSResendAttempts: maxInvitationSMSResendAttempts, - sep10SigningPrivateKey: sep10SigningPrivateKey, - crashTrackerClient: crashTrackerClient, + messageDispatcher: messageDispatcher, + Models: models, + maxInvitationResendAttempts: maxInvitationResendAttempts, + sep10SigningPrivateKey: sep10SigningPrivateKey, + crashTrackerClient: crashTrackerClient, } if err := s.validate(); err != nil { diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go index afedec71a..91f71f9ec 100644 --- a/internal/services/send_receiver_wallets_invite_service_test.go +++ b/internal/services/send_receiver_wallets_invite_service_test.go @@ -182,7 +182,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { ) mockCrashTrackerClient.On("LogAndReportErrors", ctx, mockErr, mockMsg).Once() - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -317,7 +317,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { Return(nil). Once() - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -397,7 +397,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { rec2RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet2.ID, data.ReadyReceiversWalletStatus) customInvitationMessage := "My custom receiver wallet registration invite. MyOrg ๐Ÿ‘‹" - err = models.Organizations.Update(ctx, &data.OrganizationUpdate{SMSRegistrationMessageTemplate: &customInvitationMessage}) + err = models.Organizations.Update(ctx, &data.OrganizationUpdate{ReceiverRegistrationMessageTemplate: &customInvitationMessage}) require.NoError(t, err) _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ @@ -454,7 +454,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { Return(nil). Once() - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -545,10 +545,10 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { err = dbConnectionPool.GetContext(ctx, &invitationSentAt, q, rec1RW.ID) require.NoError(t, err) - err = models.Organizations.Update(ctx, &data.OrganizationUpdate{SMSResendInterval: new(int64)}) + err = models.Organizations.Update(ctx, &data.OrganizationUpdate{ReceiverInvitationResendIntervalDays: new(int64)}) require.NoError(t, err) - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -592,7 +592,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { // Set the SMS Resend Interval var smsResendInterval int64 = 2 - err = models.Organizations.Update(ctx, &data.OrganizationUpdate{SMSResendInterval: &smsResendInterval}) + err = models.Organizations.Update(ctx, &data.OrganizationUpdate{ReceiverInvitationResendIntervalDays: &smsResendInterval}) require.NoError(t, err) _ = data.CreateMessageFixture(t, ctx, dbConnectionPool, &data.Message{ @@ -628,7 +628,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { UpdatedAt: time.Now().AddDate(0, 0, int(smsResendInterval*3)), }) - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -672,10 +672,10 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { // Set the SMS Resend Interval var smsResendInterval int64 = 2 - err = models.Organizations.Update(ctx, &data.OrganizationUpdate{SMSResendInterval: &smsResendInterval}) + err = models.Organizations.Update(ctx, &data.OrganizationUpdate{ReceiverInvitationResendIntervalDays: &smsResendInterval}) require.NoError(t, err) - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -719,7 +719,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { // Set the SMS Resend Interval var smsResendInterval int64 = 2 - err = models.Organizations.Update(ctx, &data.OrganizationUpdate{SMSResendInterval: &smsResendInterval, SMSRegistrationMessageTemplate: new(string)}) + err = models.Organizations.Update(ctx, &data.OrganizationUpdate{ReceiverInvitationResendIntervalDays: &smsResendInterval, ReceiverRegistrationMessageTemplate: new(string)}) require.NoError(t, err) walletDeepLink1 := WalletDeepLink{ @@ -742,7 +742,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { Return(nil). Once() - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -787,19 +787,19 @@ func Test_SendReceiverWalletInviteService(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, - SMSRegistrationMessageTemplate: "SMS Registration Message template test disbursement 3:", + Country: country, + Wallet: wallet1, + Status: data.ReadyDisbursementStatus, + Asset: asset1, + ReceiverRegistrationMessageTemplate: "SMS Registration Message template test disbursement 3:", }) disbursement4 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ - Country: country, - Wallet: wallet2, - Status: data.ReadyDisbursementStatus, - Asset: asset2, - SMSRegistrationMessageTemplate: "SMS Registration Message template test disbursement 4:", + Country: country, + Wallet: wallet2, + Status: data.ReadyDisbursementStatus, + Asset: asset2, + ReceiverRegistrationMessageTemplate: "SMS Registration Message template test disbursement 4:", }) s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) @@ -839,7 +839,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { } deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) - contentDisbursement3 := fmt.Sprintf("%s %s", disbursement3.SMSRegistrationMessageTemplate, deepLink1) + contentDisbursement3 := fmt.Sprintf("%s %s", disbursement3.ReceiverRegistrationMessageTemplate, deepLink1) walletDeepLink2 := WalletDeepLink{ DeepLink: wallet2.DeepLinkSchema, @@ -850,7 +850,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { } deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) - contentDisbursement4 := fmt.Sprintf("%s %s", disbursement4.SMSRegistrationMessageTemplate, deepLink2) + contentDisbursement4 := fmt.Sprintf("%s %s", disbursement4.ReceiverRegistrationMessageTemplate, deepLink2) messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ @@ -868,7 +868,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { Return(nil). Once() - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -936,11 +936,11 @@ func Test_SendReceiverWalletInviteService(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, - SMSRegistrationMessageTemplate: "SMS Registration Message template test disbursement:", + Country: country, + Wallet: wallet1, + Status: data.ReadyDisbursementStatus, + Asset: asset1, + ReceiverRegistrationMessageTemplate: "SMS Registration Message template test disbursement:", }) s, err := NewSendReceiverWalletInviteService(models, messageDispatcherMock, stellarSecretKey, 3, mockCrashTrackerClient) @@ -969,7 +969,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { // Set the SMS Resend Interval var smsResendInterval int64 = 2 - err = models.Organizations.Update(ctx, &data.OrganizationUpdate{SMSResendInterval: &smsResendInterval, SMSRegistrationMessageTemplate: new(string)}) + err = models.Organizations.Update(ctx, &data.OrganizationUpdate{ReceiverInvitationResendIntervalDays: &smsResendInterval, ReceiverRegistrationMessageTemplate: new(string)}) require.NoError(t, err) walletDeepLink1 := WalletDeepLink{ @@ -981,7 +981,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { } deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) - contentDisbursement := fmt.Sprintf("%s %s", disbursement.SMSRegistrationMessageTemplate, deepLink1) + contentDisbursement := fmt.Sprintf("%s %s", disbursement.ReceiverRegistrationMessageTemplate, deepLink1) messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ @@ -992,7 +992,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { Return(nil). Once() - reqs := []schemas.EventReceiverWalletSMSInvitationData{ + reqs := []schemas.EventReceiverWalletInvitationData{ { ReceiverWalletID: rec1RW.ID, }, @@ -1038,13 +1038,13 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { messageDispatcherMock.AssertExpectations(t) } -func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) { - var maxInvitationSMSResendAttempts int64 = 3 - s := SendReceiverWalletInviteService{maxInvitationSMSResendAttempts: maxInvitationSMSResendAttempts} +func Test_SendReceiverWalletInviteService_shouldSendInvitation(t *testing.T) { + var maxInvitationResendAttempts int64 = 3 + s := SendReceiverWalletInviteService{maxInvitationResendAttempts: maxInvitationResendAttempts} ctx := context.Background() t.Run("returns true when user never received the invitation SMS", func(t *testing.T) { - org := data.Organization{SMSResendInterval: nil} + org := data.Organization{ReceiverInvitationResendIntervalDays: nil} rwa := data.ReceiverWalletAsset{ ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: nil, @@ -1053,26 +1053,26 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) }, }, } - got := s.shouldSendInvitationSMS(ctx, &org, &rwa) + got := s.shouldSendInvitation(ctx, &org, &rwa) assert.True(t, got) }) t.Run("returns false when user received the invitation SMS and organization's SMS Resend Interval is not set", func(t *testing.T) { invitationSentAt := time.Now() - org := data.Organization{SMSResendInterval: nil} + org := data.Organization{ReceiverInvitationResendIntervalDays: nil} rwa := data.ReceiverWalletAsset{ ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: &invitationSentAt, }, } - got := s.shouldSendInvitationSMS(ctx, &org, &rwa) + got := s.shouldSendInvitation(ctx, &org, &rwa) assert.False(t, got) }) - t.Run("returns false when receiver reached the maximum number of SMS resend attempts", func(t *testing.T) { - var smsResendInterval int64 = 2 + t.Run("returns false when receiver reached the maximum number of message resend attempts", func(t *testing.T) { + var msgResendInterval int64 = 2 invitationSentAt := time.Now() - org := data.Organization{SMSResendInterval: &smsResendInterval} + org := data.Organization{ReceiverInvitationResendIntervalDays: &msgResendInterval} rwa := data.ReceiverWalletAsset{ ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: &invitationSentAt, @@ -1081,7 +1081,7 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) PhoneNumber: "+123456789", }, ReceiverWalletStats: data.ReceiverWalletStats{ - TotalInvitationSMSResentAttempts: maxInvitationSMSResendAttempts, + TotalInvitationResentAttempts: maxInvitationResendAttempts, }, }, WalletID: "wallet-ID", @@ -1089,22 +1089,22 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) getEntries := log.DefaultLogger.StartTest(log.DebugLevel) - got := s.shouldSendInvitationSMS(ctx, &org, &rwa) + got := s.shouldSendInvitation(ctx, &org, &rwa) assert.False(t, got) entries := getEntries() require.Len(t, entries, 1) assert.Equal( t, - "the invitation message was not resent to the receiver because the maximum number of SMS resend attempts has been reached: Phone Number: +12...789 - Receiver ID receiver-ID - Wallet ID wallet-ID - Total Invitation SMS resent 3 - Maximum attempts 3", + "the invitation message was not resent to the receiver because the maximum number of message resend attempts has been reached: Contact: +12...789 - Receiver ID receiver-ID - Wallet ID wallet-ID - Total Invitation resent 3 - Maximum attempts 3", entries[0].Message, ) }) - t.Run("returns false when the receiver is not in the period to resend the SMS", func(t *testing.T) { + t.Run("returns false when the receiver is not in the period to resend the message", func(t *testing.T) { var smsResendInterval int64 = 2 invitationSentAt := time.Now().AddDate(0, 0, -int(smsResendInterval-1)) - org := data.Organization{SMSResendInterval: &smsResendInterval} + org := data.Organization{ReceiverInvitationResendIntervalDays: &smsResendInterval} rwa := data.ReceiverWalletAsset{ ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: &invitationSentAt, @@ -1113,7 +1113,7 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) PhoneNumber: "+123456789", }, ReceiverWalletStats: data.ReceiverWalletStats{ - TotalInvitationSMSResentAttempts: 1, + TotalInvitationResentAttempts: 1, }, }, WalletID: "wallet-ID", @@ -1121,7 +1121,7 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) getEntries := log.DefaultLogger.StartTest(log.DebugLevel) - got := s.shouldSendInvitationSMS(ctx, &org, &rwa) + got := s.shouldSendInvitation(ctx, &org, &rwa) assert.False(t, got) entries := getEntries() @@ -1129,7 +1129,7 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) assert.Equal( t, fmt.Sprintf( - "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Phone Number: +12...789 - Receiver ID receiver-ID - Wallet ID wallet-ID - Last Invitation Sent At %s - SMS Resend Interval 2 day(s)", + "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Contact: +12...789 - Receiver ID receiver-ID - Wallet ID wallet-ID - Last Invitation Sent At %s - Receiver Invitation Resend Interval 2 day(s)", invitationSentAt.Format(time.RFC1123), ), entries[0].Message, @@ -1141,19 +1141,19 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) // 2 days after receiving the first invitation invitationSentAt := time.Now().Add((-25 * 2) * time.Hour) - org := data.Organization{SMSResendInterval: &smsResendInterval} + org := data.Organization{ReceiverInvitationResendIntervalDays: &smsResendInterval} rwa := data.ReceiverWalletAsset{ ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: &invitationSentAt, ReceiverWalletStats: data.ReceiverWalletStats{ - TotalInvitationSMSResentAttempts: 0, + TotalInvitationResentAttempts: 0, }, Receiver: data.Receiver{ PhoneNumber: "+380443973607", }, }, } - got := s.shouldSendInvitationSMS(ctx, &org, &rwa) + got := s.shouldSendInvitation(ctx, &org, &rwa) assert.True(t, got) // 4 days after receiving the first invitation @@ -1162,14 +1162,14 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: &invitationSentAt, ReceiverWalletStats: data.ReceiverWalletStats{ - TotalInvitationSMSResentAttempts: 1, + TotalInvitationResentAttempts: 1, }, Receiver: data.Receiver{ PhoneNumber: "+380443973607", }, }, } - got = s.shouldSendInvitationSMS(ctx, &org, &rwa) + got = s.shouldSendInvitation(ctx, &org, &rwa) assert.True(t, got) // 6 days after receiving the first invitation @@ -1178,14 +1178,14 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: &invitationSentAt, ReceiverWalletStats: data.ReceiverWalletStats{ - TotalInvitationSMSResentAttempts: 2, + TotalInvitationResentAttempts: 2, }, Receiver: data.Receiver{ PhoneNumber: "+380443973607", }, }, } - got = s.shouldSendInvitationSMS(ctx, &org, &rwa) + got = s.shouldSendInvitation(ctx, &org, &rwa) assert.True(t, got) // 8 days after receiving the first invitation - we don't resend because it reached the maximum number of attempts @@ -1194,14 +1194,14 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitationSMS(t *testing.T) ReceiverWallet: data.ReceiverWallet{ InvitationSentAt: &invitationSentAt, ReceiverWalletStats: data.ReceiverWalletStats{ - TotalInvitationSMSResentAttempts: 3, + TotalInvitationResentAttempts: 3, }, Receiver: data.Receiver{ PhoneNumber: "+380443973607", }, }, } - got = s.shouldSendInvitationSMS(ctx, &org, &rwa) + got = s.shouldSendInvitation(ctx, &org, &rwa) assert.False(t, got) }) } From a55924670cff009d7665b2db2760c8f7e24423e6 Mon Sep 17 00:00:00 2001 From: Marcelo Salloum dos Santos Date: Tue, 10 Sep 2024 13:31:31 -0700 Subject: [PATCH 26/75] [SDP-1296] Update `POST /wallet-registration/otp` to send OTPs through email as well (#413) ### What Update `POST /wallet-registration/otp` to send OTPs through email as well. ### Why Address https://stellarorg.atlassian.net/browse/SDP-1296 --- internal/data/receiver_verification.go | 15 +- internal/data/receiver_verification_test.go | 78 +- internal/data/receivers.go | 5 +- internal/data/receivers_wallet.go | 26 +- internal/data/receivers_wallet_test.go | 185 ++-- .../httphandler/receiver_send_otp_handler.go | 220 ++-- .../receiver_send_otp_handler_test.go | 996 ++++++++++++------ ...ifiy_receiver_registration_handler_test.go | 10 +- internal/serve/validators/mock.go | 16 + internal/utils/string.go | 11 + internal/utils/validation.go | 3 +- 11 files changed, 1048 insertions(+), 517 deletions(-) diff --git a/internal/data/receiver_verification.go b/internal/data/receiver_verification.go index 1de538a79..6ecea0ebd 100644 --- a/internal/data/receiver_verification.go +++ b/internal/data/receiver_verification.go @@ -14,6 +14,7 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/db" "github.com/stellar/stellar-disbursement-platform-backend/internal/message" + "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" ) type ReceiverVerification struct { @@ -97,29 +98,31 @@ func (m *ReceiverVerificationModel) GetAllByReceiverId(ctx context.Context, sqlE return receiverVerifications, nil } -// GetLatestByPhoneNumber returns the latest updated receiver verification for some receiver that is associated with a phone number. -func (m *ReceiverVerificationModel) GetLatestByPhoneNumber(ctx context.Context, phoneNumber string) (*ReceiverVerification, error) { - receiverVerification := ReceiverVerification{} +// GetLatestByContactInfo returns the latest updated receiver verification for a receiver associated with a phone number or email. +func (m *ReceiverVerificationModel) GetLatestByContactInfo(ctx context.Context, contactInfo string) (*ReceiverVerification, error) { query := ` SELECT rv.* FROM receiver_verifications rv - JOIN receivers r ON rv.receiver_id = r.id + JOIN receivers r ON rv.receiver_id = r.id WHERE r.phone_number = $1 + OR r.email = $1 ORDER BY rv.updated_at DESC, rv.verification_field ASC LIMIT 1 ` - err := m.dbConnectionPool.GetContext(ctx, &receiverVerification, query, phoneNumber) + receiverVerification := ReceiverVerification{} + err := m.dbConnectionPool.GetContext(ctx, &receiverVerification, query, contactInfo) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrRecordNotFound } - return nil, fmt.Errorf("fetching receiver verifications for phone number %s: %w", phoneNumber, err) + truncatedContactInfo := utils.TruncateString(contactInfo, 3) + return nil, fmt.Errorf("fetching receiver verifications for contact info %s: %w", truncatedContactInfo, err) } return &receiverVerification, nil diff --git a/internal/data/receiver_verification_test.go b/internal/data/receiver_verification_test.go index 505dac147..b4a3ba99f 100644 --- a/internal/data/receiver_verification_test.go +++ b/internal/data/receiver_verification_test.go @@ -157,7 +157,7 @@ func Test_ReceiverVerificationModel_GetReceiverVerificationByReceiverId(t *testi t.Run("returns error when the receiver has no verifications registered", func(t *testing.T) { receiverVerificationModel := ReceiverVerificationModel{dbConnectionPool: dbConnectionPool} - _, err := receiverVerificationModel.GetLatestByPhoneNumber(ctx, receiver.PhoneNumber) + _, err := receiverVerificationModel.GetLatestByContactInfo(ctx, receiver.PhoneNumber) require.Error(t, err, fmt.Errorf("cannot query any receiver verifications for phone number %s", receiver.PhoneNumber)) }) @@ -191,7 +191,7 @@ func Test_ReceiverVerificationModel_GetReceiverVerificationByReceiverId(t *testi }) receiverVerificationModel := ReceiverVerificationModel{dbConnectionPool: dbConnectionPool} - actualVerification, err := receiverVerificationModel.GetLatestByPhoneNumber(ctx, receiver.PhoneNumber) + actualVerification, err := receiverVerificationModel.GetLatestByContactInfo(ctx, receiver.PhoneNumber) require.NoError(t, err) assert.Equal(t, @@ -479,29 +479,77 @@ func Test_ReceiverVerificationModel_CheckTotalAttempts(t *testing.T) { }) } -func Test_ReceiverVerificationModel_GetLatestByPhoneNumber(t *testing.T) { +func Test_ReceiverVerificationModel_GetLatestByContactInfo(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + receiverVerificationModel := ReceiverVerificationModel{dbConnectionPool: dbConnectionPool} ctx := context.Background() - receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{}) - receiverVerificationModel := ReceiverVerificationModel{dbConnectionPool: dbConnectionPool} + oldVerificationType := VerificationTypeDateOfBirth + oldVerificationValue := "1990-01-01" + latestVerificationType := VerificationTypePin + latestVerificationValue := "123456" - err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypeDateOfBirth, "1990-01-01") - require.NoError(t, err) - err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, VerificationTypePin, "123456") - require.NoError(t, err) - - verification, err := receiverVerificationModel.GetLatestByPhoneNumber(ctx, receiver.PhoneNumber) - require.NoError(t, err) + testCases := []struct { + name string + contactInfo func(r Receiver, contactType ReceiverContactType) string + wantErrorIs error + }{ + { + name: "fails with ErrRecordNotFound when the contact info is not found", + contactInfo: func(r Receiver, contactType ReceiverContactType) string { + return "+13334445555" + }, + wantErrorIs: ErrRecordNotFound, + }, + { + name: "๐ŸŽ‰ successfully finds the latest receiver verification", + contactInfo: func(r Receiver, contactType ReceiverContactType) string { + return r.ContactByType(contactType) + }, + wantErrorIs: nil, + }, + } - assert.Equal(t, VerificationTypePin, verification.VerificationField) - assert.True(t, CompareVerificationValue(verification.HashedValue, "123456")) + for _, contactType := range GetAllReceiverContactTypes() { + receiverInsert := &Receiver{} + switch contactType { + case ReceiverContactTypeSMS: + receiverInsert.PhoneNumber = "+141555555555" + case ReceiverContactTypeEmail: + receiverInsert.Email = "foobar@test.com" + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) + defer DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool) + + receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, receiverInsert) + + err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, oldVerificationType, oldVerificationValue) + require.NoError(t, err) + err = receiverVerificationModel.UpsertVerificationValue(ctx, dbConnectionPool, receiver.ID, latestVerificationType, latestVerificationValue) + require.NoError(t, err) + + contactInfo := tc.contactInfo(*receiver, contactType) + verification, err := receiverVerificationModel.GetLatestByContactInfo(ctx, contactInfo) + if tc.wantErrorIs != nil { + require.Error(t, err) + assert.ErrorIs(t, err, ErrRecordNotFound) + assert.Nil(t, verification) + } else { + require.NoError(t, err) + assert.Equal(t, latestVerificationType, verification.VerificationField) + assert.True(t, CompareVerificationValue(verification.HashedValue, latestVerificationValue)) + } + }) + } + } } func Test_ReceiverVerification_HashAndCompareVerificationValue(t *testing.T) { diff --git a/internal/data/receivers.go b/internal/data/receivers.go index 5f66e8d66..877a237b0 100644 --- a/internal/data/receivers.go +++ b/internal/data/receivers.go @@ -43,8 +43,11 @@ func (r Receiver) ContactByType(contactType ReceiverContactType) string { } } +func GetAllReceiverContactTypes() []ReceiverContactType { + return []ReceiverContactType{ReceiverContactTypeEmail, ReceiverContactTypeSMS} +} + type ReceiverRegistrationRequest struct { - // TODO: SDP-1296 - Update `/wallet-registration/otp` to support multiple contact information types and send OTPs accordingly Email string `json:"email"` PhoneNumber string `json:"phone_number"` OTP string `json:"otp"` diff --git a/internal/data/receivers_wallet.go b/internal/data/receivers_wallet.go index 899da1172..73ceeb27f 100644 --- a/internal/data/receivers_wallet.go +++ b/internal/data/receivers_wallet.go @@ -288,20 +288,22 @@ func (rw *ReceiverWalletModel) GetAllPendingRegistrationByDisbursementID(ctx con return receiverWallets, nil } -// UpdateOTPByReceiverPhoneNumberAndWalletDomain updates receiver wallet OTP if its not verified yet, -// and returns the number of updated rows. -func (rw *ReceiverWalletModel) UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx context.Context, receiverPhoneNumber, sep10ClientDomain, otp string) (numberOfUpdatedRows int, err error) { +// UpdateOTPByReceiverContactInfoAndWalletDomain updates receiver wallet OTP if its not verified yet, and returns the +// number of updated rows. +func (rw *ReceiverWalletModel) UpdateOTPByReceiverContactInfoAndWalletDomain(ctx context.Context, receiverContactInfo, sep10ClientDomain, otp string) (numberOfUpdatedRows int, err error) { query := ` WITH rw_cte AS ( SELECT rw.id, rw.otp_confirmed_at - FROM receiver_wallets rw - INNER JOIN receivers r ON rw.receiver_id = r.id - INNER JOIN wallets w ON rw.wallet_id = w.id - WHERE r.phone_number = $1 - AND w.sep_10_client_domain = $2 - AND rw.otp_confirmed_at IS NULL + FROM + receiver_wallets rw + INNER JOIN receivers r ON rw.receiver_id = r.id + INNER JOIN wallets w ON rw.wallet_id = w.id + WHERE + (r.phone_number = $1 OR r.email = $1) + AND w.sep_10_client_domain = $2 + AND rw.otp_confirmed_at IS NULL ) UPDATE receiver_wallets @@ -313,14 +315,14 @@ func (rw *ReceiverWalletModel) UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx receiver_wallets.id = rw_cte.id ` - rows, err := rw.dbConnectionPool.ExecContext(ctx, query, receiverPhoneNumber, sep10ClientDomain, otp) + rows, err := rw.dbConnectionPool.ExecContext(ctx, query, receiverContactInfo, sep10ClientDomain, otp) if err != nil { - return 0, fmt.Errorf("error updating receiver wallets otp: %w", err) + return 0, fmt.Errorf("updating receiver wallets otp: %w", err) } updatedRowsAffected, err := rows.RowsAffected() if err != nil { - return 0, fmt.Errorf("error getting updated rows of receiver wallets otp: %w", err) + return 0, fmt.Errorf("getting updated rows of receiver wallets otp: %w", err) } return int(updatedRowsAffected), nil diff --git a/internal/data/receivers_wallet_test.go b/internal/data/receivers_wallet_test.go index a08875f1f..6ce3da101 100644 --- a/internal/data/receivers_wallet_test.go +++ b/internal/data/receivers_wallet_test.go @@ -584,82 +584,135 @@ func Test_UpdateReceiverWallet(t *testing.T) { }) } -func Test_ReceiverWallet_UpdateOTPByReceiverPhoneNumberAndWalletHomePage(t *testing.T) { +func Test_ReceiverWallet_UpdateOTPByReceiverContactInfoAndWalletDomain(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} + wallet := CreateWalletFixture(t, ctx, dbConnectionPool, "wallet", "http://home.test", "home.test", "wallet123://") - t.Run("returns 1 updated row when the receiver wallet has not confirmed yet", func(t *testing.T) { - receiver1 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{}) - wallet1 := CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "http://home.page", "home.page", "wallet1://") - _ = CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, RegisteredReceiversWalletStatus) - - testingOTP := "123456" - - rowsUpdated, err := receiverWalletModel.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, receiver1.PhoneNumber, wallet1.SEP10ClientDomain, testingOTP) - require.NoError(t, err) - assert.Equal(t, 1, rowsUpdated) - }) - - t.Run("returns 1 updated row when trying to renew an OTP with an unconfirmed receiver wallet", func(t *testing.T) { - receiver1 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{}) - receiver2 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{}) - wallet1 := CreateWalletFixture(t, ctx, dbConnectionPool, "testWalletC", "http://home3.page", "home3.page", "wallet3://") - - rw1 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, RegisteredReceiversWalletStatus) - rw2 := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet1.ID, RegisteredReceiversWalletStatus) - - testingOTP := "222333" - - q := ` - UPDATE - receiver_wallets - SET - otp_confirmed_at = NOW() - WHERE - id = $1 - ` - _, err := dbConnectionPool.ExecContext(ctx, q, rw1.ID) - require.NoError(t, err) - - rowsUpdated, err := receiverWalletModel.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, receiver2.PhoneNumber, wallet1.SEP10ClientDomain, testingOTP) - require.NoError(t, err) - assert.Equal(t, 1, rowsUpdated) - - q = `SELECT otp FROM receiver_wallets WHERE id = $1` - var dbOTP string - err = dbConnectionPool.QueryRowxContext(ctx, q, rw2.ID).Scan(&dbOTP) - require.NoError(t, err) - assert.Equal(t, testingOTP, dbOTP) - }) - - t.Run("returns 0 updated rows when the receiver wallet is confirmed", func(t *testing.T) { - receiver1 := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{}) - wallet1 := CreateWalletFixture(t, ctx, dbConnectionPool, "testWalletD", "http://home4.page", "home4.page", "wallet4://") - _ = CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, RegisteredReceiversWalletStatus) - - testingOTP := "123456" + // Define test cases + testCases := []struct { + name string + setupReceiverWallet func(t *testing.T, receiver Receiver) + contactInfo func(r Receiver, contactType ReceiverContactType) string + clientDomain string + expectedRows int + }{ + { + name: "does not update OTP for a receiver wallet with a different contact info", + setupReceiverWallet: func(t *testing.T, receiver Receiver) { + _ = CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus) + }, + contactInfo: func(r Receiver, contactType ReceiverContactType) string { + return "invalid_contact_info" + }, + clientDomain: wallet.SEP10ClientDomain, + expectedRows: 0, + }, + { + name: "does not update OTP for a receiver wallet with a different client domain", + setupReceiverWallet: func(t *testing.T, receiver Receiver) { + _ = CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus) + }, + contactInfo: func(r Receiver, contactType ReceiverContactType) string { + return r.ContactByType(contactType) + }, + clientDomain: "foo-bar", + expectedRows: 0, + }, + { + name: "does not update OTP for a confirmed receiver wallet", + setupReceiverWallet: func(t *testing.T, receiver Receiver) { + rw := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus) + // Confirm OTP + q := `UPDATE receiver_wallets SET otp_confirmed_at = NOW() WHERE id = $1` + _, err := dbConnectionPool.ExecContext(ctx, q, rw.ID) + require.NoError(t, err) + }, + contactInfo: func(r Receiver, contactType ReceiverContactType) string { + return r.ContactByType(contactType) + }, + clientDomain: wallet.SEP10ClientDomain, + expectedRows: 0, + }, + { + name: "๐ŸŽ‰ successfully updates OTP for an unconfirmed receiver wallet", + setupReceiverWallet: func(t *testing.T, receiver Receiver) { + _ = CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus) + }, + contactInfo: func(r Receiver, contactType ReceiverContactType) string { + return r.ContactByType(contactType) + }, + clientDomain: wallet.SEP10ClientDomain, + expectedRows: 1, + }, + { + name: "๐ŸŽ‰ successfully renews OTP for an unconfirmed receiver wallet", + setupReceiverWallet: func(t *testing.T, receiver Receiver) { + // Create a receiver with a different contact info toi make sure they will not be picked by the query + receiverNoOp := CreateReceiverFixture(t, ctx, dbConnectionPool, &Receiver{PhoneNumber: "+141555550000", Email: "zoopbar@test.com"}) + rwNoOp := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverNoOp.ID, wallet.ID, RegisteredReceiversWalletStatus) + + // Confirm OTP for the first receiver + q := `UPDATE receiver_wallets SET otp_confirmed_at = NOW() WHERE id = $1` + _, err := dbConnectionPool.ExecContext(ctx, q, rwNoOp.ID) + require.NoError(t, err) + + _ = CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, RegisteredReceiversWalletStatus) + }, + contactInfo: func(r Receiver, contactType ReceiverContactType) string { + return r.ContactByType(contactType) + }, + clientDomain: wallet.SEP10ClientDomain, + expectedRows: 1, + }, + } - q := ` - UPDATE - receiver_wallets - SET - otp_confirmed_at = NOW() - ` - _, err := dbConnectionPool.ExecContext(ctx, q) - require.NoError(t, err) + // Prepare test data + phoneNumber := "+141555555555" + email := "test@example.com" + + // Run test cases + for _, contactType := range GetAllReceiverContactTypes() { + receiverInsert := &Receiver{} + switch contactType { + case ReceiverContactTypeSMS: + receiverInsert.PhoneNumber = phoneNumber + case ReceiverContactTypeEmail: + receiverInsert.Email = email + } - rowsUpdated, err := receiverWalletModel.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, receiver1.PhoneNumber, wallet1.SEP10ClientDomain, testingOTP) - require.NoError(t, err) - assert.Equal(t, 0, rowsUpdated) - }) + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s/%s", contactType, tc.name), func(t *testing.T) { + defer DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) + defer DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) + + receiver := CreateReceiverFixture(t, ctx, dbConnectionPool, receiverInsert) + tc.setupReceiverWallet(t, *receiver) + + otp, err := utils.RandomString(6, utils.NumberBytes) + require.NoError(t, err) + + contactInfo := tc.contactInfo(*receiver, contactType) + rowsUpdated, err := receiverWalletModel.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, contactInfo, tc.clientDomain, otp) + require.NoError(t, err) + assert.Equal(t, tc.expectedRows, rowsUpdated) + + if tc.expectedRows > 0 { + q := `SELECT otp FROM receiver_wallets WHERE receiver_id = $1 AND wallet_id = $2` + var dbOTP string + err := dbConnectionPool.GetContext(ctx, &dbOTP, q, receiver.ID, wallet.ID) + require.NoError(t, err) + assert.Equal(t, otp, dbOTP) + } + }) + } + } } func Test_VerifyReceiverWalletOTP(t *testing.T) { @@ -1246,7 +1299,7 @@ func Test_GetByStellarAccountAndMemo(t *testing.T) { }) receiverWallet := CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, DraftReceiversWalletStatus) - results, err := receiverWalletModel.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, receiver.PhoneNumber, wallet.SEP10ClientDomain, "123456") + results, err := receiverWalletModel.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, receiver.PhoneNumber, wallet.SEP10ClientDomain, "123456") require.NoError(t, err) require.Equal(t, 1, results) diff --git a/internal/serve/httphandler/receiver_send_otp_handler.go b/internal/serve/httphandler/receiver_send_otp_handler.go index 3b321a1b5..02fbcc5ea 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler.go +++ b/internal/serve/httphandler/receiver_send_otp_handler.go @@ -1,6 +1,7 @@ package httphandler import ( + "context" "encoding/json" "errors" "fmt" @@ -19,6 +20,8 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/utils" ) +type OTPRegistrationType string + // OTPMessageDisclaimer contains disclaimer text that needs to be added as part of the OTP message to remind the // receiver how sensitive the data is. const OTPMessageDisclaimer = " If you did not request this code, please ignore. Do not share your code with anyone." @@ -36,9 +39,33 @@ type ReceiverSendOTPData struct { type ReceiverSendOTPRequest struct { PhoneNumber string `json:"phone_number"` + Email string `json:"email"` ReCAPTCHAToken string `json:"recaptcha_token"` } +// validateContactInfo validates the contact information provided in the ReceiverSendOTPRequest. It ensures that either +// the phone number or email is provided, but not both. It also validates the phone number and email format. +func (r ReceiverSendOTPRequest) validateContactInfo() validators.Validator { + v := *validators.NewValidator() + r.Email = utils.TrimAndLower(r.Email) + r.PhoneNumber = utils.TrimAndLower(r.PhoneNumber) + + switch { + case r.PhoneNumber == "" && r.Email == "": + v.Check(false, "phone_number", "phone_number or email is required") + v.Check(false, "email", "phone_number or email is required") + case r.PhoneNumber != "" && r.Email != "": + v.Check(false, "phone_number", "phone_number and email cannot be both provided") + v.Check(false, "email", "phone_number and email cannot be both provided") + case r.PhoneNumber != "": + v.CheckError(utils.ValidatePhoneNumber(r.PhoneNumber), "phone_number", "") + case r.Email != "": + v.CheckError(utils.ValidateEmail(r.Email), "email", "") + } + + return v +} + type ReceiverSendOTPResponseBody struct { Message string `json:"message"` VerificationField data.VerificationType `json:"verification_field"` @@ -47,13 +74,15 @@ type ReceiverSendOTPResponseBody struct { func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + // Parse request body receiverSendOTPRequest := ReceiverSendOTPRequest{} - err := json.NewDecoder(r.Body).Decode(&receiverSendOTPRequest) if err != nil { httperror.BadRequest("invalid request body", err, nil).Render(w) return } + receiverSendOTPRequest.PhoneNumber = utils.TrimAndLower(receiverSendOTPRequest.PhoneNumber) + receiverSendOTPRequest.Email = utils.TrimAndLower(receiverSendOTPRequest.Email) // validating reCAPTCHA Token isValid, err := h.ReCAPTCHAValidator.IsTokenValid(ctx, receiverSendOTPRequest.ReCAPTCHAToken) @@ -61,26 +90,13 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request httperror.InternalError(ctx, "Cannot validate reCAPTCHA token", err, nil).Render(w) return } - if !isValid { log.Ctx(ctx).Errorf("reCAPTCHA token is invalid") httperror.BadRequest("reCAPTCHA token is invalid", nil, nil).Render(w) return } - truncatedPhoneNumber := utils.TruncateString(receiverSendOTPRequest.PhoneNumber, 3) - if phoneValidateErr := utils.ValidatePhoneNumber(receiverSendOTPRequest.PhoneNumber); phoneValidateErr != nil { - extras := map[string]interface{}{"phone_number": "phone_number is required"} - if !errors.Is(phoneValidateErr, utils.ErrEmptyPhoneNumber) { - phoneValidateErr = fmt.Errorf("validating phone number %s: %w", truncatedPhoneNumber, phoneValidateErr) - log.Ctx(ctx).Error(phoneValidateErr) - extras["phone_number"] = "invalid phone number provided" - } - httperror.BadRequest("request invalid", phoneValidateErr, extras).Render(w) - return - } - - // Get clains from SEP24 JWT + // Validate SEP-24 JWT claims sep24Claims := anchorplatform.GetSEP24Claims(ctx) if sep24Claims == nil { err = fmt.Errorf("no SEP-24 claims found in the request context") @@ -88,7 +104,6 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request httperror.Unauthorized("", err, nil).Render(w) return } - err = sep24Claims.Valid() if err != nil { err = fmt.Errorf("SEP-24 claims are invalid: %w", err) @@ -97,77 +112,136 @@ func (h ReceiverSendOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } - verificationField := data.VerificationTypeDateOfBirth - receiverVerification, err := h.Models.ReceiverVerification.GetLatestByPhoneNumber(ctx, receiverSendOTPRequest.PhoneNumber) - if err != nil { - err = fmt.Errorf("cannot find latest receiver verification for phone number %s: %w", truncatedPhoneNumber, err) - log.Ctx(ctx).Error(err) + // Ensure XOR(PhoneNumber, Email) + if v := receiverSendOTPRequest.validateContactInfo(); v.HasErrors() { + httperror.BadRequest("", nil, v.Errors).Render(w) + return + } + + // Determine the contact type and handle accordingly + var contactType data.ReceiverContactType + var contactInfo string + if receiverSendOTPRequest.PhoneNumber != "" { + contactType, contactInfo = data.ReceiverContactTypeSMS, receiverSendOTPRequest.PhoneNumber + } else if receiverSendOTPRequest.Email != "" { + contactType, contactInfo = data.ReceiverContactTypeEmail, receiverSendOTPRequest.Email } else { - verificationField = receiverVerification.VerificationField + httperror.InternalError(ctx, "Unexpected contact info", nil, nil).Render(w) + return + } + verificationField, httpErr := h.handleOTPForReceiver(ctx, contactType, contactInfo, sep24Claims.ClientDomainClaim) + if httpErr != nil { + httpErr.Render(w) + return + } + + response := newReceiverSendOTPResponseBody(contactType, verificationField) + httpjson.RenderStatus(w, http.StatusOK, response, httpjson.JSON) +} + +// newReceiverSendOTPResponseBody creates a new ReceiverSendOTPResponseBody based on the OTP registration type and verification field. +func newReceiverSendOTPResponseBody(contactType data.ReceiverContactType, verificationField data.VerificationType) ReceiverSendOTPResponseBody { + resp := ReceiverSendOTPResponseBody{VerificationField: verificationField} + + switch contactType { + case data.ReceiverContactTypeSMS: + resp.Message = "if your phone number is registered, you'll receive an OTP" + case data.ReceiverContactTypeEmail: + resp.Message = "if your email is registered, you'll receive an OTP" + } + + return resp +} + +// handleOTPReceiver handles the OTP generation and sending for a receiver with the provided contactType and contactInfo. +func (h ReceiverSendOTPHandler) handleOTPForReceiver( + ctx context.Context, + contactType data.ReceiverContactType, + contactInfo string, + sep24ClientDomain string, +) (data.VerificationType, *httperror.HTTPError) { + var err error + placeholderVerificationField := data.VerificationTypeDateOfBirth + truncatedContactInfo := utils.TruncateString(contactInfo, 3) + contactTypeStr := utils.Humanize(string(contactType)) + + // get receiverVerification by that value of contactInfo + receiverVerification, err := h.Models.ReceiverVerification.GetLatestByContactInfo(ctx, contactInfo) + if err != nil { + log.Ctx(ctx).Warnf("Could not find ANY receiver verification for %s %s: %v", contactTypeStr, truncatedContactInfo, err) + return placeholderVerificationField, nil } // Generate a new 6 digits OTP newOTP, err := utils.RandomString(6, utils.NumberBytes) if err != nil { - httperror.InternalError(ctx, "Cannot generate OTP for receiver wallet", err, nil).Render(w) - return + return placeholderVerificationField, httperror.InternalError(ctx, "Cannot generate OTP for receiver wallet", err, nil) + } + + // Update OTP for receiver wallet + numberOfUpdatedRows, err := h.Models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, contactInfo, sep24ClientDomain, newOTP) + if err != nil && !errors.Is(err, data.ErrRecordNotFound) { + return placeholderVerificationField, httperror.InternalError(ctx, "Cannot update OTP for receiver wallet", err, nil) + } + if numberOfUpdatedRows < 1 { + log.Ctx(ctx).Warnf("Could not find a match between %s (%s) and client domain (%s)", contactTypeStr, truncatedContactInfo, sep24ClientDomain) + return placeholderVerificationField, nil } + // Send OTP message + err = h.sendOTP(ctx, contactType, contactInfo, newOTP) + if err != nil { + err = fmt.Errorf("sending OTP message: %w", err) + return placeholderVerificationField, httperror.InternalError(ctx, "Failed to send OTP message, reason: "+err.Error(), err, nil) + } + + return receiverVerification.VerificationField, nil +} + +// sendOTP sends an OTP through the provided contact type to the provided contact information. +func (h ReceiverSendOTPHandler) sendOTP(ctx context.Context, contactType data.ReceiverContactType, contactInfo, otp string) error { organization, err := h.Models.Organizations.Get(ctx) if err != nil { - httperror.InternalError(ctx, "Cannot get organization", err, nil).Render(w) - return + return fmt.Errorf("cannot get organization: %w", err) } - numberOfUpdatedRows, err := h.Models.ReceiverWallet.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, receiverSendOTPRequest.PhoneNumber, sep24Claims.ClientDomainClaim, newOTP) + otpMessageTemplate := organization.OTPMessageTemplate + OTPMessageDisclaimer + if !strings.Contains(organization.OTPMessageTemplate, "{{.OTP}}") { + // Adding the OTP code to the template + otpMessageTemplate = fmt.Sprintf(`{{.OTP}} %s`, strings.TrimSpace(otpMessageTemplate)) + } + + sendOTPMessageTpl, err := template.New("").Parse(otpMessageTemplate) if err != nil { - httperror.InternalError(ctx, "Cannot update OTP for receiver wallet", err, nil).Render(w) - return + return fmt.Errorf("cannot parse OTP template: %w", err) } - if numberOfUpdatedRows < 1 { - log.Ctx(ctx).Warnf("updated no rows in ReceiverSendOTPHandler, please verify if the provided phone number (%s) and client_domain (%s) are both valid", truncatedPhoneNumber, sep24Claims.ClientDomainClaim) - } else { - sendOTPData := ReceiverSendOTPData{ - OTP: newOTP, - OrganizationName: organization.Name, - } - - otpMessageTemplate := organization.OTPMessageTemplate + OTPMessageDisclaimer - if !strings.Contains(organization.OTPMessageTemplate, "{{.OTP}}") { - // Adding the OTP code to the template - otpMessageTemplate = fmt.Sprintf(`{{.OTP}} %s`, strings.TrimSpace(otpMessageTemplate)) - } - - sendOTPMessageTpl, err := template.New("").Parse(otpMessageTemplate) - if err != nil { - httperror.InternalError(ctx, "Cannot parse OTP template", err, nil).Render(w) - return - } - - builder := new(strings.Builder) - if err = sendOTPMessageTpl.Execute(builder, sendOTPData); err != nil { - httperror.InternalError(ctx, "Cannot execute OTP template", err, nil).Render(w) - return - } - - msg := message.Message{ - ToPhoneNumber: receiverSendOTPRequest.PhoneNumber, - Message: builder.String(), - } - - // TODO: SDP-1296 - support multiple channels for OTP - log.Ctx(ctx).Infof("sending OTP message to phone number: %s", truncatedPhoneNumber) - err = h.MessageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority) - if err != nil { - httperror.InternalError(ctx, "Cannot send OTP message", err, nil).Render(w) - return - } - } - - response := ReceiverSendOTPResponseBody{ - Message: "if your phone number is registered, you'll receive an OTP", - VerificationField: verificationField, + sendOTPData := ReceiverSendOTPData{ + OTP: otp, + OrganizationName: organization.Name, } - httpjson.RenderStatus(w, http.StatusOK, response, httpjson.JSON) + + builder := new(strings.Builder) + if err = sendOTPMessageTpl.Execute(builder, sendOTPData); err != nil { + return fmt.Errorf("cannot execute OTP template: %w", err) + } + + msg := message.Message{Message: builder.String()} + switch contactType { + case data.ReceiverContactTypeSMS: + msg.ToPhoneNumber = contactInfo + case data.ReceiverContactTypeEmail: + msg.ToEmail = contactInfo + msg.Title = "Your One-Time Password: " + otp + } + + truncatedContactInfo := utils.TruncateString(contactInfo, 3) + contactTypeStr := utils.Humanize(string(contactType)) + log.Ctx(ctx).Infof("sending OTP message to %s %s...", contactTypeStr, truncatedContactInfo) + err = h.MessageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority) + if err != nil { + return fmt.Errorf("cannot send OTP message through %s to %s: %w", contactTypeStr, truncatedContactInfo, err) + } + + return nil } diff --git a/internal/serve/httphandler/receiver_send_otp_handler_test.go b/internal/serve/httphandler/receiver_send_otp_handler_test.go index 308747a8a..1cc07576b 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler_test.go +++ b/internal/serve/httphandler/receiver_send_otp_handler_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -14,6 +15,8 @@ import ( "github.com/go-chi/chi/v5" "github.com/golang-jwt/jwt/v4" + "github.com/sirupsen/logrus" + "github.com/stellar/go/support/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -23,371 +26,688 @@ import ( "github.com/stellar/stellar-disbursement-platform-backend/internal/anchorplatform" "github.com/stellar/stellar-disbursement-platform-backend/internal/data" "github.com/stellar/stellar-disbursement-platform-backend/internal/message" + "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" ) -func Test_ReceiverSendOTPHandler_ServeHTTP(t *testing.T) { - r := chi.NewRouter() +func Test_ReceiverSendOTPRequest_validateContactInfo(t *testing.T) { + testCases := []struct { + name string + receiverSendOTPRequest ReceiverSendOTPRequest + wantValidationErrors map[string]interface{} + }{ + { + name: "๐Ÿ”ด phone number and email both empty", + receiverSendOTPRequest: ReceiverSendOTPRequest{ + PhoneNumber: "", + Email: "", + }, + wantValidationErrors: map[string]interface{}{ + "phone_number": "phone_number or email is required", + "email": "phone_number or email is required", + }, + }, + { + name: "๐Ÿ”ด phone number and email both provided", + receiverSendOTPRequest: ReceiverSendOTPRequest{ + PhoneNumber: "+141555550000", + Email: "foobar@test.com", + }, + wantValidationErrors: map[string]interface{}{ + "phone_number": "phone_number and email cannot be both provided", + "email": "phone_number and email cannot be both provided", + }, + }, + { + name: "๐Ÿ”ด phone number is invalid", + receiverSendOTPRequest: ReceiverSendOTPRequest{ + PhoneNumber: "invalid", + }, + wantValidationErrors: map[string]interface{}{ + "phone_number": "the provided phone number is not a valid E.164 number", + }, + }, + { + name: "๐Ÿ”ด email is invalid", + receiverSendOTPRequest: ReceiverSendOTPRequest{ + Email: "invalid", + }, + wantValidationErrors: map[string]interface{}{ + "email": "the provided email is not valid", + }, + }, + { + name: "๐ŸŸข phone number is valid", + receiverSendOTPRequest: ReceiverSendOTPRequest{ + PhoneNumber: "+14155550000", + }, + wantValidationErrors: nil, + }, + { + name: "๐ŸŸข email is valid", + receiverSendOTPRequest: ReceiverSendOTPRequest{ + Email: "foobar@test.com", + }, + wantValidationErrors: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + v := tc.receiverSendOTPRequest.validateContactInfo() + if len(tc.wantValidationErrors) == 0 { + assert.Len(t, v.Errors, 0) + } else { + assert.Equal(t, tc.wantValidationErrors, v.Errors) + } + }) + } +} + +func Test_ReceiverSendOTPHandler_ServeHTTP_validation(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) - ctx := context.Background() - - phoneNumber := "+380443973607" - receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) - receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{}) - wallet1 := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://home.page", "home.page", "wallet123://") - data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ - ReceiverID: receiver1.ID, - VerificationField: data.VerificationTypeDateOfBirth, - }) - - _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.RegisteredReceiversWalletStatus) - _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet1.ID, data.RegisteredReceiversWalletStatus) - - mockMessageDispatcher := message.NewMockMessageDispatcher(t) - reCAPTCHAValidator := &validators.ReCAPTCHAValidatorMock{} - - r.Post("/wallet-registration/otp", ReceiverSendOTPHandler{ - Models: models, - MessageDispatcher: mockMessageDispatcher, - ReCAPTCHAValidator: reCAPTCHAValidator, - }.ServeHTTP) - - requestSendOTP := ReceiverSendOTPRequest{ - PhoneNumber: receiver1.PhoneNumber, - ReCAPTCHAToken: "XyZ", + validClaims := &anchorplatform.SEP24JWTClaims{ + ClientDomainClaim: "no-op-domain.test.com", + RegisteredClaims: jwt.RegisteredClaims{ + ID: "test-transaction-id", + Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + }, } - reqBody, err := json.Marshal(requestSendOTP) - require.NoError(t, err) - - t.Run("returns 401 - Unauthorized if the token is not in the request context", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(true, nil). - Once() - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - - resp := rr.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assert.JSONEq(t, `{"error":"Not authorized."}`, string(respBody)) - }) - - t.Run("returns 401 - Unauthorized if the token is in the request context but it's not valid", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(true, nil). - Once() - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - - rr := httptest.NewRecorder() - invalidClaims := &anchorplatform.SEP24JWTClaims{} - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, invalidClaims)) - r.ServeHTTP(rr, req) - - resp := rr.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) - assert.JSONEq(t, `{"error":"Not authorized."}`, string(respBody)) - }) - - t.Run("returns 400 - BadRequest with a wrong request body", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(true, nil). - Twice() - invalidRequest := `{"recaptcha_token": "XyZ"}` - - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(invalidRequest)) - require.NoError(t, err) - - rr := httptest.NewRecorder() - invalidClaims := &anchorplatform.SEP24JWTClaims{} - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, invalidClaims)) - r.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":"request invalid","extras":{"phone_number":"phone_number is required"}}`, string(respBody)) - - req, err = http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(`{"phone_number": "+55555555555", "recaptcha_token": "XyZ"}`)) - require.NoError(t, err) - - rr = httptest.NewRecorder() - invalidClaims = &anchorplatform.SEP24JWTClaims{} - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, invalidClaims)) - r.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": "request invalid", "extras": {"phone_number": "invalid phone number provided"}}`, string(respBody)) - }) - - t.Run("returns 200 - Ok if the token is in the request context and body is valid", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(true, nil). - Once() - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - - validClaims := &anchorplatform.SEP24JWTClaims{ - ClientDomainClaim: wallet1.SEP10ClientDomain, - RegisteredClaims: jwt.RegisteredClaims{ - ID: "test-transaction-id", - Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", - ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + ctxWithValidSEP24Claims := context.WithValue(ctx, anchorplatform.SEP24ClaimsContextKey, validClaims) + invalidClaims := &anchorplatform.SEP24JWTClaims{} + ctxWithInvalidSEP24Claims := context.WithValue(ctx, anchorplatform.SEP24ClaimsContextKey, invalidClaims) + + const reCAPTCHAToken = "XyZ" + + testCases := []struct { + name string + context context.Context + receiverSendOTPRequest ReceiverSendOTPRequest + prepareMocksFn func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, mockMessageDispatcher *message.MockMessageDispatcher) + wantStatusCode int + wantBody string + }{ + { + name: "(500 - InternalServerError) if the reCAPTCHA validation returns an error", + context: ctx, + receiverSendOTPRequest: ReceiverSendOTPRequest{ReCAPTCHAToken: "invalid-recaptcha-token"}, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, _ *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, "invalid-recaptcha-token"). + Return(false, errors.New("invalid recaptcha")). + Once() }, - } - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - - mockMessageDispatcher. - On("SendMessage", - mock.Anything, - mock.AnythingOfType("message.Message"), - []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). - Once(). - Run(func(args mock.Arguments) { - msg := args.Get(1).(message.Message) - assert.Contains(t, msg.Message, "is your MyCustomAid phone verification code.") - assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message) - }) - - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - - resp := rr.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Contains(t, resp.Header.Get("Content-Type"), "/json; charset=utf-8") - assert.JSONEq(t, string(respBody), `{"message":"if your phone number is registered, you'll receive an OTP", "verification_field":"DATE_OF_BIRTH"}`) - }) - - t.Run("returns 200 - parses a custom OTP message template successfully", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(true, nil). - Once() - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - - validClaims := &anchorplatform.SEP24JWTClaims{ - ClientDomainClaim: wallet1.SEP10ClientDomain, - RegisteredClaims: jwt.RegisteredClaims{ - ID: "test-transaction-id", - Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", - ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + wantStatusCode: http.StatusInternalServerError, + wantBody: `{"error":"Cannot validate reCAPTCHA token"}`, + }, + { + name: "(400 - BadRequest) if the reCAPTCHA token is invalid", + context: ctx, + receiverSendOTPRequest: ReceiverSendOTPRequest{ReCAPTCHAToken: reCAPTCHAToken}, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, _ *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, reCAPTCHAToken). + Return(false, nil). + Once() }, - } - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - - // Set a custom message for the OTP message - customOTPMessage := "Here's your code to complete your registration. MyOrg ๐Ÿ‘‹" - err = models.Organizations.Update(ctx, &data.OrganizationUpdate{OTPMessageTemplate: &customOTPMessage}) - require.NoError(t, err) - - mockMessageDispatcher. - On("SendMessage", - mock.Anything, - mock.AnythingOfType("message.Message"), - []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). - Once(). - Run(func(args mock.Arguments) { - msg := args.Get(1).(message.Message) - assert.Contains(t, msg.Message, customOTPMessage) - assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message) - }) - - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - - resp := rr.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Contains(t, resp.Header.Get("Content-Type"), "/json; charset=utf-8") - assert.JSONEq(t, string(respBody), `{"message":"if your phone number is registered, you'll receive an OTP", "verification_field":"DATE_OF_BIRTH"}`) - }) - - t.Run("returns 500 - InternalServerError when something goes wrong when sending the SMS", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(true, nil). - Once() - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - - validClaims := &anchorplatform.SEP24JWTClaims{ - ClientDomainClaim: wallet1.SEP10ClientDomain, - RegisteredClaims: jwt.RegisteredClaims{ - ID: "test-transaction-id", - Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", - ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + wantStatusCode: http.StatusBadRequest, + wantBody: `{"error":"reCAPTCHA token is invalid"}`, + }, + { + name: "(401 - Unauthorized) if the SEP-24 claims are not in the request context", + context: ctx, + receiverSendOTPRequest: ReceiverSendOTPRequest{ReCAPTCHAToken: reCAPTCHAToken}, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, _ *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, reCAPTCHAToken). + Return(true, nil). + Once() }, - } - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - - mockMessageDispatcher. - On("SendMessage", - mock.Anything, - mock.AnythingOfType("message.Message"), - []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(errors.New("error sending message")). - Once() - - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - - resp := rr.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - assert.Contains(t, resp.Header.Get("Content-Type"), "/json; charset=utf-8") - assert.JSONEq(t, string(respBody), `{"error":"Cannot send OTP message"}`) - }) - - t.Run("returns 500 - InternalServerError when unable to validate recaptcha", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(false, errors.New("error requesting verify reCAPTCHA token")). - Once() - - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - - validClaims := &anchorplatform.SEP24JWTClaims{ - ClientDomainClaim: wallet1.SEP10ClientDomain, - RegisteredClaims: jwt.RegisteredClaims{ - ID: "test-transaction-id", - Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", - ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + wantStatusCode: http.StatusUnauthorized, + wantBody: `{"error":"Not authorized."}`, + }, + { + name: "(401 - Unauthorized) if the SEP-24 claims are invalid", + context: ctxWithInvalidSEP24Claims, + receiverSendOTPRequest: ReceiverSendOTPRequest{ReCAPTCHAToken: reCAPTCHAToken}, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, _ *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, reCAPTCHAToken). + Return(true, nil). + Once() }, - } - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) + wantStatusCode: http.StatusUnauthorized, + wantBody: `{"error":"Not authorized."}`, + }, + { + name: "(400 - BadRequest) if the request body is invalid", + context: ctxWithValidSEP24Claims, + receiverSendOTPRequest: ReceiverSendOTPRequest{ReCAPTCHAToken: reCAPTCHAToken}, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, _ *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, reCAPTCHAToken). + Return(true, nil). + Once() + }, + wantStatusCode: http.StatusBadRequest, + wantBody: `{ + "error": "The request was invalid in some way.", + "extras": { + "phone_number":"phone_number or email is required", + "email":"phone_number or email is required" + } + }`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockReCAPTCHAValidator := validators.NewReCAPTCHAValidatorMock(t) + mockMessageDispatcher := message.NewMockMessageDispatcher(t) + + tc.prepareMocksFn(t, mockReCAPTCHAValidator, mockMessageDispatcher) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) + r := chi.NewRouter() + r.Post("/wallet-registration/otp", ReceiverSendOTPHandler{ + Models: models, + MessageDispatcher: mockMessageDispatcher, + ReCAPTCHAValidator: mockReCAPTCHAValidator, + }.ServeHTTP) - resp := w.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) + reqBody, err := json.Marshal(tc.receiverSendOTPRequest) + require.NoError(t, err) + req, err := http.NewRequestWithContext(tc.context, http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) + require.NoError(t, err) + rr := httptest.NewRecorder() - wantsBody := ` - { - "error": "Cannot validate reCAPTCHA token" + r.ServeHTTP(rr, req) + + resp := rr.Result() + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.Equal(t, tc.wantStatusCode, resp.StatusCode) + assert.JSONEq(t, tc.wantBody, string(respBody)) + }) + } +} + +func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(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) + const phoneNumber = "+14155550000" + const email = "foobar@test.com" + require.NoError(t, err) + wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://correct.test", "correct.test", "wallet123://") + + validClaims := &anchorplatform.SEP24JWTClaims{ + ClientDomainClaim: wallet.SEP10ClientDomain, + RegisteredClaims: jwt.RegisteredClaims{ + ID: "test-transaction-id", + Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), + }, + } + ctxWithValidSEP24Claims := context.WithValue(ctx, anchorplatform.SEP24ClaimsContextKey, validClaims) + + const reCAPTCHAToken = "XyZ" + + type testCase struct { + name string + receiverSendOTPRequest ReceiverSendOTPRequest + verificationField data.VerificationType + contactType data.ReceiverContactType + prepareMocksFn func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, mockMessageDispatcher *message.MockMessageDispatcher) + shouldCreateObjects bool + assertLogsFn func(t *testing.T, contactType data.ReceiverContactType, r data.Receiver, entries []logrus.Entry) + wantStatusCode int + wantBody string + } + testCases := []testCase{} + + for _, contactType := range data.GetAllReceiverContactTypes() { + for _, verificationField := range data.GetAllVerificationTypes() { + receiverSendOTPRequest := ReceiverSendOTPRequest{ReCAPTCHAToken: reCAPTCHAToken} + var contactInfo string + switch contactType { + case data.ReceiverContactTypeSMS: + receiverSendOTPRequest.PhoneNumber = phoneNumber + contactInfo = phoneNumber + case data.ReceiverContactTypeEmail: + receiverSendOTPRequest.Email = email + contactInfo = email } - ` - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - assert.JSONEq(t, wantsBody, string(respBody)) - }) - - t.Run("returns 200 (DoB) - InternalServerError if phone number is not associated with receiver verification", func(t *testing.T) { - requestSendOTP := ReceiverSendOTPRequest{ - PhoneNumber: "+14152223333", - ReCAPTCHAToken: "XyZ", + truncatedContactInfo := utils.TruncateString(contactInfo, 3) + + testCases = append(testCases, []testCase{ + { + name: fmt.Sprintf("%s/%s/๐Ÿ”ด (500-InternalServerError) when the SMS dispatcher fails", contactType, verificationField), + receiverSendOTPRequest: receiverSendOTPRequest, + verificationField: verificationField, + contactType: contactType, + shouldCreateObjects: true, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, mockMessageDispatcher *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, reCAPTCHAToken). + Return(true, nil). + Once() + mockMessageDispatcher. + On("SendMessage", + mock.Anything, + mock.AnythingOfType("message.Message"), + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). + Return(errors.New("failed calling message dispatcher")). + Once(). + Run(func(args mock.Arguments) { + msg := args.Get(1).(message.Message) + assert.Contains(t, msg.Message, "is your MyCustomAid phone verification code.") + assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message) + }) + }, + assertLogsFn: func(t *testing.T, contactType data.ReceiverContactType, r data.Receiver, entries []logrus.Entry) { + contactTypeStr := utils.Humanize(string(contactType)) + wantLog := fmt.Sprintf("sending OTP message to %s %s", contactTypeStr, truncatedContactInfo) + assert.Contains(t, entries[0].Message, wantLog) + }, + wantStatusCode: http.StatusInternalServerError, + wantBody: fmt.Sprintf(`{"error":"Failed to send OTP message, reason: sending OTP message: cannot send OTP message through %s to %s: failed calling message dispatcher"}`, utils.Humanize(string(contactType)), truncatedContactInfo), + }, + { + name: fmt.Sprintf("%s/%s/๐ŸŸก (200-Ok) with false positive", contactType, verificationField), + receiverSendOTPRequest: receiverSendOTPRequest, + verificationField: verificationField, + contactType: contactType, + shouldCreateObjects: false, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, mockMessageDispatcher *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, reCAPTCHAToken). + Return(true, nil). + Once() + }, + assertLogsFn: func(t *testing.T, contactType data.ReceiverContactType, r data.Receiver, entries []logrus.Entry) { + contactTypeStr := utils.Humanize(string(contactType)) + wantLog := fmt.Sprintf("Could not find ANY receiver verification for %s %s: %v", contactTypeStr, truncatedContactInfo, data.ErrRecordNotFound) + assert.Contains(t, entries[0].Message, wantLog) + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf(`{"message":"if your %s is registered, you'll receive an OTP","verification_field":"DATE_OF_BIRTH"}`, utils.Humanize(string(contactType))), + }, + { + name: fmt.Sprintf("%s/%s/๐ŸŸข (200-Ok) OTP sent!", contactType, verificationField), + receiverSendOTPRequest: receiverSendOTPRequest, + verificationField: verificationField, + contactType: contactType, + shouldCreateObjects: true, + prepareMocksFn: func(t *testing.T, mockReCAPTCHAValidator *validators.ReCAPTCHAValidatorMock, mockMessageDispatcher *message.MockMessageDispatcher) { + mockReCAPTCHAValidator. + On("IsTokenValid", mock.Anything, reCAPTCHAToken). + Return(true, nil). + Once() + mockMessageDispatcher. + On("SendMessage", + mock.Anything, + mock.AnythingOfType("message.Message"), + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). + Return(nil). + Once(). + Run(func(args mock.Arguments) { + msg := args.Get(1).(message.Message) + assert.Contains(t, msg.Message, "is your MyCustomAid phone verification code.") + assert.Regexp(t, regexp.MustCompile(`^\d{6}\s.+$`), msg.Message) + }) + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf(`{"message":"if your %s is registered, you'll receive an OTP","verification_field":"%s"}`, utils.Humanize(string(contactType)), verificationField), + }, + }...) } - reqBody, _ = json.Marshal(requestSendOTP) - - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(true, nil). - Once() - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - - validClaims := &anchorplatform.SEP24JWTClaims{ - ClientDomainClaim: wallet1.SEP10ClientDomain, - RegisteredClaims: jwt.RegisteredClaims{ - ID: "test-transaction-id", - Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", - ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), - }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) + defer data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) + defer data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool) + if tc.shouldCreateObjects { + receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{ + PhoneNumber: tc.receiverSendOTPRequest.PhoneNumber, + Email: tc.receiverSendOTPRequest.Email, + }) + data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.RegisteredReceiversWalletStatus) + data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ + ReceiverID: receiver.ID, + VerificationField: tc.verificationField, + }) + } + + mockReCAPTCHAValidator := validators.NewReCAPTCHAValidatorMock(t) + mockMessageDispatcher := message.NewMockMessageDispatcher(t) + + tc.prepareMocksFn(t, mockReCAPTCHAValidator, mockMessageDispatcher) + + r := chi.NewRouter() + r.Post("/wallet-registration/otp", ReceiverSendOTPHandler{ + Models: models, + MessageDispatcher: mockMessageDispatcher, + ReCAPTCHAValidator: mockReCAPTCHAValidator, + }.ServeHTTP) + + reqBody, err := json.Marshal(tc.receiverSendOTPRequest) + require.NoError(t, err) + req, err := http.NewRequestWithContext(ctxWithValidSEP24Claims, http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) + require.NoError(t, err) + rr := httptest.NewRecorder() + + getEntries := log.DefaultLogger.StartTest(logrus.DebugLevel) + r.ServeHTTP(rr, req) + + resp := rr.Result() + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.Equal(t, tc.wantStatusCode, resp.StatusCode) + assert.JSONEq(t, tc.wantBody, string(respBody)) + entries := getEntries() + if tc.assertLogsFn != nil { + tc.assertLogsFn(t, tc.contactType, data.Receiver{}, entries) + } + }) + } +} + +func Test_newReceiverSendOTPResponseBody(t *testing.T) { + for _, otpType := range data.GetAllReceiverContactTypes() { + for _, verificationType := range data.GetAllVerificationTypes() { + t.Run(fmt.Sprintf("%s/%s", otpType, verificationType), func(t *testing.T) { + gotBody := newReceiverSendOTPResponseBody(otpType, verificationType) + wantBody := ReceiverSendOTPResponseBody{ + Message: fmt.Sprintf("if your %s is registered, you'll receive an OTP", utils.Humanize(string(otpType))), + VerificationField: verificationType, + } + require.Equal(t, wantBody, gotBody) + }) } - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) - - rr := httptest.NewRecorder() - r.ServeHTTP(rr, req) - - resp := rr.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - wantsBody := `{ - "message":"if your phone number is registered, you'll receive an OTP", - "verification_field":"DATE_OF_BIRTH" - }` - assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.JSONEq(t, wantsBody, string(respBody)) - }) - - t.Run("returns 400 - BadRequest when recaptcha token is invalid", func(t *testing.T) { - reCAPTCHAValidator. - On("IsTokenValid", mock.Anything, "XyZ"). - Return(false, nil). - Once() - - req, err := http.NewRequest(http.MethodPost, "/wallet-registration/otp", strings.NewReader(string(reqBody))) - require.NoError(t, err) - - validClaims := &anchorplatform.SEP24JWTClaims{ - ClientDomainClaim: wallet1.SEP10ClientDomain, - RegisteredClaims: jwt.RegisteredClaims{ - ID: "test-transaction-id", - Subject: "GBLTXF46JTCGMWFJASQLVXMMA36IPYTDCN4EN73HRXCGDCGYBZM3A444", - ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), - }, + } +} + +func Test_ReceiverSendOTPHandler_sendOTP(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() + organization, err := models.Organizations.Get(ctx) + require.NoError(t, err) + defaultOTPMessageTemplate := organization.OTPMessageTemplate + + phoneNumber := "+380443973607" + email := "foobar@test.com" + otp := "246810" + + testCases := []struct { + name string + overrideOrgOTPTemplate string + wantMessage string + shouldDispatcherFail bool + }{ + { + 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), + }, + { + 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), + }, + { + name: "๐ŸŽ‰ successful with custom message and pre-existing OTP tag", + overrideOrgOTPTemplate: "Here's your code: {{.OTP}}.", + wantMessage: "Here's your code: 246810. If you did not request this code, please ignore. Do not share your code with anyone.", + }, + { + name: "๐ŸŽ‰ successful with custom message and NO pre-existing OTP tag", + overrideOrgOTPTemplate: "is your one-time password.", + wantMessage: "246810 is your one-time password. If you did not request this code, please ignore. Do not share your code with anyone.", + }, + } + + for _, contactType := range data.GetAllReceiverContactTypes() { + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s/%s", contactType, tc.name), func(t *testing.T) { + var expectedMsg message.Message + var contactInfo string + switch contactType { + case data.ReceiverContactTypeSMS: + expectedMsg = message.Message{ToPhoneNumber: phoneNumber, Message: tc.wantMessage} + contactInfo = phoneNumber + case data.ReceiverContactTypeEmail: + expectedMsg = message.Message{ToEmail: email, Message: tc.wantMessage, Title: "Your One-Time Password: " + otp} + contactInfo = email + } + + mockMessageDispatcher := message.NewMockMessageDispatcher(t) + mockCall := mockMessageDispatcher. + On("SendMessage", + mock.Anything, + expectedMsg, + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}) + if !tc.shouldDispatcherFail { + mockCall.Return(nil).Once() + } else { + mockCall.Return(errors.New("error sending message")).Once() + } + + handler := ReceiverSendOTPHandler{ + Models: models, + MessageDispatcher: mockMessageDispatcher, + } + + err = models.Organizations.Update(ctx, &data.OrganizationUpdate{ + OTPMessageTemplate: &tc.overrideOrgOTPTemplate, + }) + require.NoError(t, err) + + err := handler.sendOTP(ctx, contactType, contactInfo, otp) + require.NoError(t, err) + }) } - req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, validClaims)) + } +} - w := httptest.NewRecorder() - r.ServeHTTP(w, req) +func Test_ReceiverSendOTPHandler_handleOTPForReceiver(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() - resp := w.Result() - respBody, err := io.ReadAll(resp.Body) - require.NoError(t, err) + models, err := data.NewModels(dbConnectionPool) + require.NoError(t, err) - wantsBody := ` - { - "error": "reCAPTCHA token is invalid" - } - ` - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - assert.JSONEq(t, wantsBody, string(respBody)) - }) + ctx := context.Background() + wallet := data.CreateWalletFixture(t, ctx, dbConnectionPool, "testWallet", "https://correct.test", "correct.test", "wallet123://") + receiverWithoutWalletInsert := &data.Receiver{ + PhoneNumber: "+141555550000", + Email: "without_wallet@test.com", + } - mockMessageDispatcher.AssertExpectations(t) - reCAPTCHAValidator.AssertExpectations(t) + testCases := []struct { + name string + contactInfo func(r data.Receiver, contactType data.ReceiverContactType) string + dateOfBirth string + sep24ClientDomain string + prepareMocksFn func(t *testing.T, mockMessageDispatcher *message.MockMessageDispatcher) + assertLogsFn func(t *testing.T, contactType data.ReceiverContactType, r data.Receiver, entries []logrus.Entry) + wantVerificationField data.VerificationType + wantHttpErr func(contactType data.ReceiverContactType, r data.Receiver) *httperror.HTTPError + }{ + { + name: "๐ŸŸก false positive if GetLatestByContactInfo returns no results", + contactInfo: func(r data.Receiver, contactType data.ReceiverContactType) string { + return "not_found" + }, + assertLogsFn: func(t *testing.T, contactType data.ReceiverContactType, r data.Receiver, entries []logrus.Entry) { + contactTypeStr := utils.Humanize(string(contactType)) + truncatedContactInfo := utils.TruncateString("not_found", 3) + wantLog := fmt.Sprintf("Could not find ANY receiver verification for %s %s: %v", contactTypeStr, truncatedContactInfo, data.ErrRecordNotFound) + assert.Contains(t, entries[0].Message, wantLog) + }, + wantVerificationField: data.VerificationTypeDateOfBirth, + }, + { + name: "๐ŸŸก false positive if UpdateOTPByReceiverContactInfoAndWalletDomain doesn't find a {,client_domain} match (client_domain)", + contactInfo: func(r data.Receiver, contactType data.ReceiverContactType) string { + return r.ContactByType(contactType) + }, + sep24ClientDomain: "incorrect.test", + assertLogsFn: func(t *testing.T, contactType data.ReceiverContactType, r data.Receiver, entries []logrus.Entry) { + contactTypeStr := utils.Humanize(string(contactType)) + truncatedContactInfo := utils.TruncateString(r.ContactByType(contactType), 3) + wantLog := fmt.Sprintf("Could not find a match between %s (%s) and client domain (%s)", contactTypeStr, truncatedContactInfo, "incorrect.test") + assert.Contains(t, entries[0].Message, wantLog) + }, + wantVerificationField: data.VerificationTypeDateOfBirth, + }, + { + name: "๐ŸŸก false positive if UpdateOTPByReceiverContactInfoAndWalletDomain doesn't find a {,client_domain} match ()", + contactInfo: func(_ data.Receiver, contactType data.ReceiverContactType) string { + return receiverWithoutWalletInsert.ContactByType(contactType) + }, + sep24ClientDomain: "correct.test", + assertLogsFn: func(t *testing.T, contactType data.ReceiverContactType, _ data.Receiver, entries []logrus.Entry) { + contactTypeStr := utils.Humanize(string(contactType)) + truncatedContactInfo := utils.TruncateString(receiverWithoutWalletInsert.ContactByType(contactType), 3) + wantLog := fmt.Sprintf("Could not find a match between %s (%s) and client domain (%s)", contactTypeStr, truncatedContactInfo, "correct.test") + assert.Contains(t, entries[0].Message, wantLog) + }, + wantVerificationField: data.VerificationTypeDateOfBirth, + }, + { + name: "๐Ÿ”ด error if sendOTP fails", + contactInfo: func(r data.Receiver, contactType data.ReceiverContactType) string { + return r.ContactByType(contactType) + }, + sep24ClientDomain: "correct.test", + prepareMocksFn: func(t *testing.T, mockMessageDispatcher *message.MockMessageDispatcher) { + mockMessageDispatcher. + On("SendMessage", + mock.Anything, + mock.AnythingOfType("message.Message"), + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). + Return(errors.New("error sending message")). + Once() + }, + wantVerificationField: data.VerificationTypeDateOfBirth, + wantHttpErr: func(contactType data.ReceiverContactType, r data.Receiver) *httperror.HTTPError { + contactTypeStr := utils.Humanize(string(contactType)) + truncatedContactInfo := utils.TruncateString(r.ContactByType(contactType), 3) + err := fmt.Errorf("sending OTP message: %w", fmt.Errorf("cannot send OTP message through %s to %s: %w", contactTypeStr, truncatedContactInfo, errors.New("error sending message"))) + return httperror.InternalError(ctx, "Failed to send OTP message, reason: "+err.Error(), err, nil) + }, + }, + { + name: "๐ŸŸข successful", + contactInfo: func(r data.Receiver, contactType data.ReceiverContactType) string { + return r.ContactByType(contactType) + }, + sep24ClientDomain: "correct.test", + prepareMocksFn: func(t *testing.T, mockMessageDispatcher *message.MockMessageDispatcher) { + mockMessageDispatcher. + On("SendMessage", + mock.Anything, + mock.AnythingOfType("message.Message"), + []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). + Return(nil). + Once() + }, + wantVerificationField: data.VerificationTypePin, + wantHttpErr: nil, + }, + } + + for _, contactType := range data.GetAllReceiverContactTypes() { + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s/%s", contactType, tc.name), func(t *testing.T) { + receiverWithWalletInsert := &data.Receiver{} + switch contactType { + case data.ReceiverContactTypeSMS: + receiverWithWalletInsert.PhoneNumber = "+141555551111" + case data.ReceiverContactTypeEmail: + receiverWithWalletInsert.Email = "with_wallet@test.com" + } + + defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) + defer data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) + defer data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool) + + handler := ReceiverSendOTPHandler{Models: models} + if tc.prepareMocksFn != nil { + mockMessageDispatcher := message.NewMockMessageDispatcher(t) + tc.prepareMocksFn(t, mockMessageDispatcher) + handler.MessageDispatcher = mockMessageDispatcher + } + + // Setup receiver with Verification but without wallet: + receiverWithoutWallet := data.CreateReceiverFixture(t, ctx, dbConnectionPool, receiverWithoutWalletInsert) + _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ + ReceiverID: receiverWithoutWallet.ID, + VerificationField: data.VerificationTypePin, + VerificationValue: "123456", + }) + + // Setup receiver with Verification AND wallet: + receiverWithWallet := data.CreateReceiverFixture(t, ctx, dbConnectionPool, receiverWithWalletInsert) + _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ + ReceiverID: receiverWithWallet.ID, + VerificationField: data.VerificationTypePin, + VerificationValue: "123456", + }) + _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverWithWallet.ID, wallet.ID, data.RegisteredReceiversWalletStatus) + + getEntries := log.DefaultLogger.StartTest(logrus.DebugLevel) + + contactInfo := tc.contactInfo(*receiverWithWallet, contactType) + verificationField, httpErr := handler.handleOTPForReceiver(ctx, contactType, contactInfo, tc.sep24ClientDomain) + if tc.wantHttpErr != nil { + wantHTTPErr := tc.wantHttpErr(contactType, *receiverWithWallet) + require.NotNil(t, httpErr) + assert.Equal(t, *wantHTTPErr, *httpErr) + assert.Equal(t, tc.wantVerificationField, verificationField) + } else { + require.Nil(t, httpErr) + assert.Equal(t, tc.wantVerificationField, verificationField) + } + + entries := getEntries() + if tc.assertLogsFn != nil { + tc.assertLogsFn(t, contactType, *receiverWithWallet, entries) + } + }) + } + } } diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go index 0af138841..135ac59a8 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go +++ b/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go @@ -1024,7 +1024,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin VerificationValue: "1990-01-01", }) _ = data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) - _, err := models.ReceiverWallet.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") + _, err := models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") require.NoError(t, err) // set the logger to a buffer so we can check the error message @@ -1104,7 +1104,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin VerificationValue: "1990-01-01", }) receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) - _, err := models.ReceiverWallet.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") + _, err := models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") require.NoError(t, err) // setup router and execute request @@ -1209,7 +1209,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin VerificationValue: "1990-01-01", }) receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) - _, err := models.ReceiverWallet.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") + _, err := models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") require.NoError(t, err) // setup router and execute request @@ -1258,7 +1258,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin // registering Second Wallet receiverWallet2 := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet2.ID, data.ReadyReceiversWalletStatus) - _, err = models.ReceiverWallet.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, "+380445555555", wallet2.SEP10ClientDomain, "123456") + _, err = models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, "+380445555555", wallet2.SEP10ClientDomain, "123456") require.NoError(t, err) sep24Claims.ClientDomainClaim = wallet2.SEP10ClientDomain @@ -1327,7 +1327,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin VerificationValue: "1990-01-01", }) receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) - _, err := models.ReceiverWallet.UpdateOTPByReceiverPhoneNumberAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") + _, err := models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") require.NoError(t, err) // Creating a payment ready to pay diff --git a/internal/serve/validators/mock.go b/internal/serve/validators/mock.go index a799f56bc..e0d4b93d6 100644 --- a/internal/serve/validators/mock.go +++ b/internal/serve/validators/mock.go @@ -14,3 +14,19 @@ func (v *ReCAPTCHAValidatorMock) IsTokenValid(ctx context.Context, token string) args := v.Called(ctx, token) return args.Bool(0), args.Error(1) } + +type testInterface interface { + mock.TestingT + Cleanup(func()) +} + +// NewReCAPTCHAValidatorMock creates a new instance of ReCAPTCHAValidatorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewReCAPTCHAValidatorMock(t testInterface) *ReCAPTCHAValidatorMock { + mock := &ReCAPTCHAValidatorMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/utils/string.go b/internal/utils/string.go index e072e336d..df2f77f0b 100644 --- a/internal/utils/string.go +++ b/internal/utils/string.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "fmt" "math/big" + "strings" ) const ( @@ -38,3 +39,13 @@ func TruncateString(str string, borderSizeToKeep int) string { } return str[:borderSizeToKeep] + "..." + str[len(str)-borderSizeToKeep:] } + +// TrimAndLower trims and lowercases a string. +func TrimAndLower(str string) string { + return strings.TrimSpace(strings.ToLower(str)) +} + +// Humanize converts a string to a human readable format. +func Humanize(str string) string { + return strings.ToLower(strings.ReplaceAll(str, "_", " ")) +} diff --git a/internal/utils/validation.go b/internal/utils/validation.go index 0a7591656..7e7342571 100644 --- a/internal/utils/validation.go +++ b/internal/utils/validation.go @@ -16,6 +16,7 @@ var ( rxOTP = regexp.MustCompile(`^\d{6}$`) ErrInvalidE164PhoneNumber = fmt.Errorf("the provided phone number is not a valid E.164 number") ErrEmptyPhoneNumber = fmt.Errorf("phone number cannot be empty") + ErrEmptyEmail = fmt.Errorf("email cannot be empty") ) const ( @@ -67,7 +68,7 @@ var rxEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9 func ValidateEmail(email string) error { if email == "" { - return fmt.Errorf("email cannot be empty") + return ErrEmptyEmail } if !rxEmail.MatchString(email) { From dd37bcccbbd8ea1c18c8b17c0b32621edbde9259 Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Mon, 16 Sep 2024 11:20:17 -0700 Subject: [PATCH 27/75] SDP-1316 Update send and auto-retry invitation scheduler job to work with both SMS and email (#415) --- internal/data/fixtures.go | 29 ++++- internal/message/message_dispatcher.go | 16 ++- internal/message/message_dispatcher_test.go | 32 +++--- .../mock_message_dispatcher_interface.go | 20 +++- ...eceiver_wallets_sms_invitation_job_test.go | 22 ++-- .../httphandler/receiver_send_otp_handler.go | 2 +- .../receiver_send_otp_handler_test.go | 18 ++- .../send_receiver_wallets_invite_service.go | 71 ++++-------- ...nd_receiver_wallets_invite_service_test.go | 104 ++++++++++-------- 9 files changed, 178 insertions(+), 136 deletions(-) diff --git a/internal/data/fixtures.go b/internal/data/fixtures.go index 6f5edb802..dc7c3760b 100644 --- a/internal/data/fixtures.go +++ b/internal/data/fixtures.go @@ -287,6 +287,8 @@ 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) require.NoError(t, err) @@ -336,6 +338,31 @@ func CreateReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExec return &receiver } +func InsertReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, r *ReceiverInsert) *Receiver { + t.Helper() + + if r.ExternalId == nil { + randString, err := utils.RandomString(56) + require.NoError(t, err) + r.ExternalId = &randString + } + + const query = ` + INSERT INTO receivers + (email, phone_number, external_id) + VALUES + ($1, $2, $3) + RETURNING + id, COALESCE(phone_number, '') as phone_number, COALESCE(email, '') as email, external_id, created_at, updated_at + ` + + var receiver Receiver + err := sqlExec.GetContext(ctx, &receiver, query, r.Email, r.PhoneNumber, r.ExternalId) + require.NoError(t, err) + + return &receiver +} + func DeleteAllReceiversFixtures(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter) { const query = "DELETE FROM receivers" _, err := sqlExec.ExecContext(ctx, query) @@ -419,7 +446,7 @@ func CreateReceiverWalletFixture(t *testing.T, ctx context.Context, sqlExec db.S SELECT rw.id, rw.stellar_address, rw.stellar_memo, rw.stellar_memo_type, rw.status, rw.status_history, rw.created_at, rw.updated_at, rw.anchor_platform_transaction_id, rw.anchor_platform_transaction_synced_at, - r.id, r.email, r.phone_number, r.external_id, r.created_at, r.updated_at, + r.id, COALESCE(r.phone_number, '') as phone_number, COALESCE(r.email, '') as email, r.external_id, r.created_at, r.updated_at, w.id, w.name, w.homepage, w.deep_link_schema, w.created_at, w.updated_at FROM inserted_receiver_wallet AS rw diff --git a/internal/message/message_dispatcher.go b/internal/message/message_dispatcher.go index c0c142133..803b40969 100644 --- a/internal/message/message_dispatcher.go +++ b/internal/message/message_dispatcher.go @@ -17,7 +17,7 @@ const ( //go:generate mockery --name MessageDispatcherInterface --case=underscore --structname=MockMessageDispatcher --inpackage type MessageDispatcherInterface interface { RegisterClient(ctx context.Context, channel MessageChannel, client MessengerClient) - SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error + SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) (MessengerType, error) GetClient(channel MessageChannel) (MessengerClient, error) } @@ -36,14 +36,17 @@ func (d *MessageDispatcher) RegisterClient(ctx context.Context, channel MessageC d.clients[channel] = client } -func (d *MessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error { +func (d *MessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) (MessengerType, error) { + // default to the highest priority channel messenger type. + messengerType := d.clients[channelPriority[0]].MessengerType() + supportedChannels := make(map[MessageChannel]bool) for _, ch := range message.SupportedChannels() { supportedChannels[ch] = true } if len(supportedChannels) == 0 { - return fmt.Errorf("no valid channel found for message %s", message) + return messengerType, fmt.Errorf("no valid channel found for message %s", message) } for _, channel := range channelPriority { @@ -57,16 +60,17 @@ func (d *MessageDispatcher) SendMessage(ctx context.Context, message Message, ch log.Ctx(ctx).Warnf("No client registered for channel %q", channel) continue } + messengerType = client.MessengerType() err := client.SendMessage(message) if err == nil { - return nil + return messengerType, nil } - log.Ctx(ctx).Errorf("Error sending message %s using channel %q: %v", message, channel, err) + log.Ctx(ctx).Errorf("Error sending %s through messenger type %s: %v", channel, messengerType, err) } - return fmt.Errorf("unable to send message %s using any of the supported channels [%v]", message, supportedChannels) + return messengerType, fmt.Errorf("unable to send message %s using any of the supported channels [%v]", message, supportedChannels) } func (d *MessageDispatcher) GetClient(channel MessageChannel) (MessengerClient, error) { diff --git a/internal/message/message_dispatcher_test.go b/internal/message/message_dispatcher_test.go index bb62c7fe5..0d9faea90 100644 --- a/internal/message/message_dispatcher_test.go +++ b/internal/message/message_dispatcher_test.go @@ -90,12 +90,13 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) { emptyMessage := Message{} tests := []struct { - name string - message Message - channelPriority []MessageChannel - supportedChannels []MessageChannel - setupMock func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) - expectedErr error + name string + message Message + channelPriority []MessageChannel + supportedChannels []MessageChannel + setupMock func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock) + expectedMessengerType MessengerType + expectedErr error }{ { name: "fail when no supported channels", @@ -129,7 +130,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) { smsClientMock.AssertNotCalled(t, "SendMessage", emailMessage) }, - expectedErr: nil, + expectedMessengerType: MessengerTypeAWSEmail, + expectedErr: nil, }, { name: "successful when single supported channel (sms)", @@ -144,7 +146,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) { emailClientMock.AssertNotCalled(t, "SendMessage", smsMessage) }, - expectedErr: nil, + expectedMessengerType: MessengerTypeTwilioSMS, + expectedErr: nil, }, { name: "successful when multiple supported channels", @@ -159,7 +162,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) { emailClientMock.AssertNotCalled(t, "SendMessage", multiChannelMessage) }, - expectedErr: nil, + expectedMessengerType: MessengerTypeTwilioSMS, + expectedErr: nil, }, { name: "successful when first channel fails (sms) but second succeeds (e-mail)", @@ -177,7 +181,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) { Return(nil). Once() }, - expectedErr: nil, + expectedMessengerType: MessengerTypeAWSEmail, + expectedErr: nil, }, { name: "fail when all channels fail", @@ -205,19 +210,20 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) { dispatcher := NewMessageDispatcher() emailClient := NewMessengerClientMock(t) - emailClient.On("MessengerType").Return(MessengerTypeDryRun).Once() + emailClient.On("MessengerType").Return(MessengerTypeAWSEmail).Maybe() dispatcher.RegisterClient(ctx, MessageChannelEmail, emailClient) smsClient := NewMessengerClientMock(t) - smsClient.On("MessengerType").Return(MessengerTypeDryRun).Once() + smsClient.On("MessengerType").Return(MessengerTypeTwilioSMS).Maybe() dispatcher.RegisterClient(ctx, MessageChannelSMS, smsClient) tt.setupMock(emailClient, smsClient) - err := dispatcher.SendMessage(ctx, tt.message, tt.channelPriority) + messengerType, err := dispatcher.SendMessage(ctx, tt.message, tt.channelPriority) if tt.expectedErr != nil { assert.EqualError(t, err, tt.expectedErr.Error()) } else { + assert.Equal(t, tt.expectedMessengerType, messengerType) assert.NoError(t, err) } }) diff --git a/internal/message/mock_message_dispatcher_interface.go b/internal/message/mock_message_dispatcher_interface.go index aceda8185..cb3642273 100644 --- a/internal/message/mock_message_dispatcher_interface.go +++ b/internal/message/mock_message_dispatcher_interface.go @@ -49,21 +49,31 @@ func (_m *MockMessageDispatcher) RegisterClient(ctx context.Context, channel Mes } // SendMessage provides a mock function with given fields: ctx, message, channelPriority -func (_m *MockMessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error { +func (_m *MockMessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) (MessengerType, error) { ret := _m.Called(ctx, message, channelPriority) if len(ret) == 0 { panic("no return value specified for SendMessage") } - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, Message, []MessageChannel) error); ok { + var r0 MessengerType + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, Message, []MessageChannel) (MessengerType, error)); ok { + return rf(ctx, message, channelPriority) + } + if rf, ok := ret.Get(0).(func(context.Context, Message, []MessageChannel) MessengerType); ok { r0 = rf(ctx, message, channelPriority) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(MessengerType) } - return r0 + if rf, ok := ret.Get(1).(func(context.Context, Message, []MessageChannel) error); ok { + r1 = rf(ctx, message, channelPriority) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // NewMockMessageDispatcher creates a new instance of MockMessageDispatcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 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 10f60bb8c..df7834e01 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 @@ -140,8 +140,6 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { t.Run("executes the service successfully", func(t *testing.T) { messageDispatcherMock := message.NewMockMessageDispatcher(t) - messengerClientMock := message.NewMessengerClientMock(t) - crashTrackerClientMock := &crashtracker.MockCrashTrackerClient{} s, err := services.NewSendReceiverWalletInviteService( @@ -217,6 +215,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { 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, @@ -228,30 +227,27 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { 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("GetClient", message.MessageChannelSMS). - Return(messengerClientMock, nil). - Twice(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, ToEmail: receiver1.Email, Message: contentWallet1, + Title: titleWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(mockErr). + Return(message.MessengerTypeTwilioSMS, mockErr). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, ToEmail: receiver2.Email, Message: contentWallet2, + Title: titleWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once() - messengerClientMock. - On("MessengerType"). - Return(message.MessengerTypeTwilioSMS). - Twice() + 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, @@ -279,7 +275,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { assert.Equal(t, wallet1.ID, msg.WalletID) assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.FailureMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + 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) @@ -295,7 +291,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) { assert.Equal(t, wallet2.ID, msg.WalletID) assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + 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) diff --git a/internal/serve/httphandler/receiver_send_otp_handler.go b/internal/serve/httphandler/receiver_send_otp_handler.go index 02fbcc5ea..de7bd0cc1 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler.go +++ b/internal/serve/httphandler/receiver_send_otp_handler.go @@ -238,7 +238,7 @@ func (h ReceiverSendOTPHandler) sendOTP(ctx context.Context, contactType data.Re truncatedContactInfo := utils.TruncateString(contactInfo, 3) contactTypeStr := utils.Humanize(string(contactType)) log.Ctx(ctx).Infof("sending OTP message to %s %s...", contactTypeStr, truncatedContactInfo) - err = h.MessageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority) + _, err = h.MessageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority) if err != nil { return fmt.Errorf("cannot send OTP message through %s to %s: %w", contactTypeStr, truncatedContactInfo, err) } diff --git a/internal/serve/httphandler/receiver_send_otp_handler_test.go b/internal/serve/httphandler/receiver_send_otp_handler_test.go index 1cc07576b..1ddcb0571 100644 --- a/internal/serve/httphandler/receiver_send_otp_handler_test.go +++ b/internal/serve/httphandler/receiver_send_otp_handler_test.go @@ -287,13 +287,16 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) { for _, verificationField := range data.GetAllVerificationTypes() { receiverSendOTPRequest := ReceiverSendOTPRequest{ReCAPTCHAToken: reCAPTCHAToken} var contactInfo string + var messengerType message.MessengerType switch contactType { case data.ReceiverContactTypeSMS: receiverSendOTPRequest.PhoneNumber = phoneNumber contactInfo = phoneNumber + messengerType = message.MessengerTypeTwilioSMS case data.ReceiverContactTypeEmail: receiverSendOTPRequest.Email = email contactInfo = email + messengerType = message.MessengerTypeAWSEmail } truncatedContactInfo := utils.TruncateString(contactInfo, 3) @@ -314,7 +317,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) { mock.Anything, mock.AnythingOfType("message.Message"), []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(errors.New("failed calling message dispatcher")). + Return(messengerType, errors.New("failed calling message dispatcher")). Once(). Run(func(args mock.Arguments) { msg := args.Get(1).(message.Message) @@ -366,7 +369,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) { mock.Anything, mock.AnythingOfType("message.Message"), []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(messengerType, nil). Once(). Run(func(args mock.Arguments) { msg := args.Get(1).(message.Message) @@ -501,13 +504,16 @@ func Test_ReceiverSendOTPHandler_sendOTP(t *testing.T) { t.Run(fmt.Sprintf("%s/%s", contactType, tc.name), func(t *testing.T) { var expectedMsg message.Message var contactInfo string + var messengerType message.MessengerType switch contactType { case data.ReceiverContactTypeSMS: expectedMsg = message.Message{ToPhoneNumber: phoneNumber, Message: tc.wantMessage} contactInfo = phoneNumber + messengerType = message.MessengerTypeTwilioSMS case data.ReceiverContactTypeEmail: expectedMsg = message.Message{ToEmail: email, Message: tc.wantMessage, Title: "Your One-Time Password: " + otp} contactInfo = email + messengerType = message.MessengerTypeAWSEmail } mockMessageDispatcher := message.NewMockMessageDispatcher(t) @@ -517,9 +523,9 @@ func Test_ReceiverSendOTPHandler_sendOTP(t *testing.T) { expectedMsg, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}) if !tc.shouldDispatcherFail { - mockCall.Return(nil).Once() + mockCall.Return(messengerType, nil).Once() } else { - mockCall.Return(errors.New("error sending message")).Once() + mockCall.Return(messengerType, errors.New("error sending message")).Once() } handler := ReceiverSendOTPHandler{ @@ -619,7 +625,7 @@ func Test_ReceiverSendOTPHandler_handleOTPForReceiver(t *testing.T) { mock.Anything, mock.AnythingOfType("message.Message"), []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(errors.New("error sending message")). + Return(message.MessengerTypeTwilioSMS, errors.New("error sending message")). Once() }, wantVerificationField: data.VerificationTypeDateOfBirth, @@ -642,7 +648,7 @@ func Test_ReceiverSendOTPHandler_handleOTPForReceiver(t *testing.T) { mock.Anything, mock.AnythingOfType("message.Message"), []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once() }, wantVerificationField: data.VerificationTypePin, diff --git a/internal/services/send_receiver_wallets_invite_service.go b/internal/services/send_receiver_wallets_invite_service.go index e05b9faa2..f543bf18d 100644 --- a/internal/services/send_receiver_wallets_invite_service.go +++ b/internal/services/send_receiver_wallets_invite_service.go @@ -56,7 +56,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive currentTenant, err := tenant.GetTenantFromContext(ctx) if err != nil { - return fmt.Errorf("error getting tenant from context: %w", err) + return fmt.Errorf("getting tenant from context: %w", err) } if currentTenant.BaseURL == nil { return fmt.Errorf("tenant base URL cannot be nil for tenant %s", currentTenant.ID) @@ -65,7 +65,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive // Get the organization entry to get the Org name and ReceiverRegistrationMessageTemplate organization, err := s.Models.Organizations.Get(ctx) if err != nil { - return fmt.Errorf("error getting organization: %w", err) + return fmt.Errorf("getting organization: %w", err) } // Debug purposes @@ -81,12 +81,12 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive // Execute the template early so we avoid hitting the database to query the other info msgTemplate, err := template.New("").Parse(orgReceiverRegistrationMessageTemplate) if err != nil { - return fmt.Errorf("error parsing organization receiver registration message template: %w", err) + return fmt.Errorf("parsing organization receiver registration message template: %w", err) } wallets, err := s.Models.Wallets.GetAll(ctx) if err != nil { - return fmt.Errorf("error getting all wallets: %w", err) + return fmt.Errorf("getting all wallets: %w", err) } walletsMap := make(map[string]data.Wallet, len(wallets)) @@ -96,12 +96,12 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive receiverWallets, err := s.resolveReceiverWalletsPendingRegistration(ctx, receiverWalletInvitationData) if err != nil { - return fmt.Errorf("error resolving receiver wallets pending registration: %w", err) + return fmt.Errorf("resolving receiver wallets pending registration: %w", err) } receiverWalletsAsset, err := s.Models.Assets.GetAssetsPerReceiverWallet(ctx, receiverWallets...) if err != nil { - return fmt.Errorf("error getting all assets: %w", err) + return fmt.Errorf("getting all assets: %w", err) } msgsToInsert := []*data.MessageInsert{} @@ -155,47 +155,36 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive return fmt.Errorf("executing registration message template: %w", err) } - // TODO: SDP-1316 - add a Title. Consider if we should make the title configurable. - msg := message.Message{ - Message: content.String(), - } + msg := message.Message{Message: content.String()} if rwa.ReceiverWallet.Receiver.PhoneNumber != "" { msg.ToPhoneNumber = rwa.ReceiverWallet.Receiver.PhoneNumber } if rwa.ReceiverWallet.Receiver.Email != "" { msg.ToEmail = rwa.ReceiverWallet.Receiver.Email + msg.Title = "You have a payment waiting for you from " + organization.Name } - assetID := rwa.Asset.ID - receiverWalletID := rwa.ReceiverWallet.ID - - // TODO: SDP-1316 - Update send and auto-retry invitation scheduler job to work with both SMS and email - messageChannel := message.MessageChannelSMS - messageClient, err := s.messageDispatcher.GetClient(messageChannel) - if err != nil { - return fmt.Errorf("getting message client: %w", err) - } - - messageType := messageClient.MessengerType() msgToInsert := &data.MessageInsert{ - Type: messageType, - AssetID: &assetID, + AssetID: &rwa.Asset.ID, ReceiverID: rwa.ReceiverWallet.Receiver.ID, WalletID: wallet.ID, - ReceiverWalletID: &receiverWalletID, - TextEncrypted: content.String(), + ReceiverWalletID: &rwa.ReceiverWallet.ID, + TextEncrypted: msg.Message, + TitleEncrypted: msg.Title, } - // We assume that the message will be sent at first - msgToInsert.Status = data.SuccessMessageStatus - if err := s.messageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority); err != nil { + if messengerType, sendErr := s.messageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority); sendErr != nil { errMsg := fmt.Sprintf( "error sending message to receiver ID %s for receiver wallet ID %s using messenger type %s", - rwa.ReceiverWallet.Receiver.ID, rwa.ReceiverWallet.ID, messageType, + rwa.ReceiverWallet.Receiver.ID, rwa.ReceiverWallet.ID, messengerType, ) // call crash tracker client to log and report error - s.crashTrackerClient.LogAndReportErrors(ctx, err, errMsg) + s.crashTrackerClient.LogAndReportErrors(ctx, sendErr, errMsg) msgToInsert.Status = data.FailureMessageStatus + msgToInsert.Type = messengerType + } else { + msgToInsert.Status = data.SuccessMessageStatus + msgToInsert.Type = messengerType } msgsToInsert = append(msgsToInsert, msgToInsert) @@ -214,7 +203,7 @@ func (s SendReceiverWalletInviteService) SendInvite(ctx context.Context, receive } if err := s.Models.Message.BulkInsert(ctx, dbTx, msgsToInsert); err != nil { - return fmt.Errorf("error inserting messages in the database: %w", err) + return fmt.Errorf("inserting messages in the database: %w", err) } return nil @@ -250,16 +239,6 @@ func (s SendReceiverWalletInviteService) resolveReceiverWalletsPendingRegistrati func (s SendReceiverWalletInviteService) shouldSendInvitation(ctx context.Context, organization *data.Organization, rwa *data.ReceiverWalletAsset) bool { receiver := rwa.ReceiverWallet.Receiver - // TODO: SDP-1316 - add support for other contact information in this method. - var phoneNumber string - if receiver.PhoneNumber == "" { - return false - } else { - phoneNumber = receiver.PhoneNumber - } - - truncatedReceiverContact := utils.TruncateString(phoneNumber, 3) - // We've never sent an Invitation message if rwa.ReceiverWallet.InvitationSentAt == nil { return true @@ -268,8 +247,8 @@ func (s SendReceiverWalletInviteService) shouldSendInvitation(ctx context.Contex // If organization's Receiver Invitation Resend Interval is nil and we've sent the invitation message to the receiver, we won't resend it. if organization.ReceiverInvitationResendIntervalDays == nil && rwa.ReceiverWallet.InvitationSentAt != nil { log.Ctx(ctx).Debugf( - "the invitation message was not automatically resent to the receiver %s with contact %s because the organization's Receiver Invitation Resend Interval is nil", - receiver.ID, truncatedReceiverContact) + "the invitation message was not automatically resent to the receiver %s because the organization's Receiver Invitation Resend Interval is nil", + receiver.ID) return false } @@ -278,8 +257,7 @@ func (s SendReceiverWalletInviteService) shouldSendInvitation(ctx context.Contex // Check if the receiver wallet reached the maximum number of resend attempts. if rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationResentAttempts >= s.maxInvitationResendAttempts { log.Ctx(ctx).Debugf( - "the invitation message was not resent to the receiver because the maximum number of message resend attempts has been reached: Contact: %s - Receiver ID %s - Wallet ID %s - Total Invitation resent %d - Maximum attempts %d", - truncatedReceiverContact, + "the invitation message was not resent to the receiver because the maximum number of message resend attempts has been reached: Receiver ID %s - Wallet ID %s - Total Invitation resent %d - Maximum attempts %d", receiver.ID, rwa.WalletID, rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationResentAttempts, @@ -293,8 +271,7 @@ func (s SendReceiverWalletInviteService) shouldSendInvitation(ctx context.Contex AddDate(0, 0, -int(*organization.ReceiverInvitationResendIntervalDays*(rwa.ReceiverWallet.ReceiverWalletStats.TotalInvitationResentAttempts+1))) if !rwa.ReceiverWallet.InvitationSentAt.Before(resendPeriod) { log.Ctx(ctx).Debugf( - "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Contact: %s - Receiver ID %s - Wallet ID %s - Last Invitation Sent At %s - Receiver Invitation Resend Interval %d day(s)", - truncatedReceiverContact, + "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Receiver ID %s - Wallet ID %s - Last Invitation Sent At %s - Receiver Invitation Resend Interval %d day(s)", receiver.ID, rwa.WalletID, rwa.ReceiverWallet.InvitationSentAt.Format(time.RFC1123), diff --git a/internal/services/send_receiver_wallets_invite_service_test.go b/internal/services/send_receiver_wallets_invite_service_test.go index 91f71f9ec..54cf8d970 100644 --- a/internal/services/send_receiver_wallets_invite_service_test.go +++ b/internal/services/send_receiver_wallets_invite_service_test.go @@ -51,7 +51,7 @@ func Test_GetSignedRegistrationLink_SchemelessDeepLink(t *testing.T) { require.Equal(t, wantRegistrationLink, registrationLink) } -func Test_SendReceiverWalletInviteService(t *testing.T) { +func Test_SendReceiverWalletInviteService_SendInvite(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() @@ -64,14 +64,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { ctx := tenant.SaveTenantInContext(context.Background(), tenantInfo) stellarSecretKey := "SBUSPEKAZKLZSWHRSJ2HWDZUK6I3IVDUWA7JJZSGBLZ2WZIUJI7FPNB5" - messageClientMock := message.NewMessengerClientMock(t) - messageClientMock. - On("MessengerType"). - Return(message.MessengerTypeTwilioSMS) messageDispatcherMock := message.NewMockMessageDispatcher(t) - messageDispatcherMock. - On("GetClient", message.MessageChannelSMS). - Return(messageClientMock, nil) mockCrashTrackerClient := &crashtracker.MockCrashTrackerClient{} @@ -88,6 +81,12 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{}) receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{}) + receiverEmailOnly := data.InsertReceiverFixture(t, ctx, dbConnectionPool, &data.ReceiverInsert{ + Email: utils.StringPtr("emailJWP5O@randomemail.com"), + }) + receiverPhoneOnly := data.InsertReceiverFixture(t, ctx, dbConnectionPool, &data.ReceiverInsert{ + PhoneNumber: utils.StringPtr("1234567890"), + }) disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ Country: country, @@ -147,6 +146,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { 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 := WalletDeepLink{ DeepLink: wallet2.DeepLinkSchema, @@ -158,6 +158,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { 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. @@ -165,15 +166,17 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { ToPhoneNumber: receiver1.PhoneNumber, ToEmail: receiver1.Email, Message: contentWallet1, + Title: titleWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(errors.New("unexpected error")). + Return(message.MessengerTypeTwilioSMS, errors.New("unexpected error")). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, ToEmail: receiver2.Email, Message: contentWallet2, + Title: titleWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once() mockMsg := fmt.Sprintf( @@ -224,7 +227,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet1.ID, msg.WalletID) assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.FailureMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + 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) @@ -240,7 +243,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet2.ID, msg.WalletID) assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + 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) @@ -258,10 +261,10 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { data.DeleteAllMessagesFixtures(t, ctx, dbConnectionPool) data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) - rec1RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet1.ID, data.ReadyReceiversWalletStatus) - data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet2.ID, data.RegisteredReceiversWalletStatus) + rec1RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverPhoneOnly.ID, wallet1.ID, data.ReadyReceiversWalletStatus) + data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverPhoneOnly.ID, wallet2.ID, data.RegisteredReceiversWalletStatus) - rec2RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet2.ID, data.ReadyReceiversWalletStatus) + rec2RW := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiverEmailOnly.ID, wallet2.ID, data.ReadyReceiversWalletStatus) _ = data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ Status: data.ReadyPaymentStatus, @@ -289,6 +292,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { 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 := WalletDeepLink{ DeepLink: wallet2.DeepLinkSchema, @@ -300,21 +304,21 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { 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 messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ - ToPhoneNumber: receiver1.PhoneNumber, - ToEmail: receiver1.Email, + ToPhoneNumber: receiverPhoneOnly.PhoneNumber, Message: contentWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once(). On("SendMessage", mock.Anything, message.Message{ - ToPhoneNumber: receiver2.PhoneNumber, - ToEmail: receiver2.Email, - Message: contentWallet2, + ToEmail: receiverEmailOnly.Email, + Message: contentWallet2, + Title: titleWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeAWSEmail, nil). Once() reqs := []schemas.EventReceiverWalletInvitationData{ @@ -329,13 +333,13 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { err = s.SendInvite(ctx, reqs...) require.NoError(t, err) - receivers, err := models.ReceiverWallet.GetByReceiverIDsAndWalletID(ctx, dbConnectionPool, []string{receiver1.ID}, wallet1.ID) + receivers, err := models.ReceiverWallet.GetByReceiverIDsAndWalletID(ctx, dbConnectionPool, []string{receiverPhoneOnly.ID}, wallet1.ID) require.NoError(t, err) require.Len(t, receivers, 1) assert.Equal(t, rec1RW.ID, receivers[0].ID) assert.NotNil(t, receivers[0].InvitationSentAt) - receivers, err = models.ReceiverWallet.GetByReceiverIDsAndWalletID(ctx, dbConnectionPool, []string{receiver2.ID}, wallet2.ID) + receivers, err = models.ReceiverWallet.GetByReceiverIDsAndWalletID(ctx, dbConnectionPool, []string{receiverEmailOnly.ID}, wallet2.ID) require.NoError(t, err) require.Len(t, receivers, 1) assert.Equal(t, rec2RW.ID, receivers[0].ID) @@ -351,11 +355,11 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { 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) + err = dbConnectionPool.GetContext(ctx, &msg, q, receiverPhoneOnly.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, receiverPhoneOnly.ID, msg.ReceiverID) assert.Equal(t, wallet1.ID, msg.WalletID) assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) @@ -367,15 +371,15 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Nil(t, msg.AssetID) msg = data.Message{} - err = dbConnectionPool.GetContext(ctx, &msg, q, receiver2.ID, wallet2.ID, rec2RW.ID) + err = dbConnectionPool.GetContext(ctx, &msg, q, receiverEmailOnly.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, message.MessengerTypeAWSEmail, msg.Type) + assert.Equal(t, receiverEmailOnly.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.Empty(t, msg.TitleEncrypted) + 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) @@ -426,6 +430,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) contentWallet1 := fmt.Sprintf("%s %s", customInvitationMessage, deepLink1) + titleWallet1 := "You have a payment waiting for you from " + walletDeepLink1.OrganizationName walletDeepLink2 := WalletDeepLink{ DeepLink: wallet2.DeepLinkSchema, @@ -437,21 +442,24 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) contentWallet2 := fmt.Sprintf("%s %s", customInvitationMessage, deepLink2) + titleWallet2 := "You have a payment waiting for you from " + walletDeepLink2.OrganizationName messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, ToEmail: receiver1.Email, Message: contentWallet1, + Title: titleWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, ToEmail: receiver2.Email, Message: contentWallet2, + Title: titleWallet2, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once() reqs := []schemas.EventReceiverWalletInvitationData{ @@ -496,7 +504,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet1.ID, msg.WalletID) assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + 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) @@ -512,7 +520,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet2.ID, msg.WalletID) assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + assert.Equal(t, titleWallet1, msg.TitleEncrypted) assert.Equal(t, contentWallet2, msg.TextEncrypted) assert.Len(t, msg.StatusHistory, 2) assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status) @@ -732,14 +740,16 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { 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 messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, ToEmail: receiver1.Email, Message: contentWallet1, + Title: titleWallet1, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once() reqs := []schemas.EventReceiverWalletInvitationData{ @@ -777,7 +787,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet1.ID, msg.WalletID) assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + 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) @@ -840,6 +850,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) contentDisbursement3 := fmt.Sprintf("%s %s", disbursement3.ReceiverRegistrationMessageTemplate, deepLink1) + titleDisbursement3 := "You have a payment waiting for you from " + walletDeepLink1.OrganizationName walletDeepLink2 := WalletDeepLink{ DeepLink: wallet2.DeepLinkSchema, @@ -851,21 +862,24 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) contentDisbursement4 := fmt.Sprintf("%s %s", disbursement4.ReceiverRegistrationMessageTemplate, deepLink2) + titleDisbursement4 := "You have a payment waiting for you from " + walletDeepLink2.OrganizationName messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, ToEmail: receiver1.Email, Message: contentDisbursement3, + Title: titleDisbursement3, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once(). On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver2.PhoneNumber, ToEmail: receiver2.Email, Message: contentDisbursement4, + Title: titleDisbursement4, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once() reqs := []schemas.EventReceiverWalletInvitationData{ @@ -910,7 +924,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet1.ID, msg.WalletID) assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + assert.Equal(t, titleDisbursement3, msg.TitleEncrypted) assert.Equal(t, contentDisbursement3, msg.TextEncrypted) assert.Len(t, msg.StatusHistory, 2) assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status) @@ -926,7 +940,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet2.ID, msg.WalletID) assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + assert.Equal(t, titleDisbursement4, msg.TitleEncrypted) assert.Equal(t, contentDisbursement4, msg.TextEncrypted) assert.Len(t, msg.StatusHistory, 2) assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status) @@ -982,14 +996,16 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey) require.NoError(t, err) contentDisbursement := fmt.Sprintf("%s %s", disbursement.ReceiverRegistrationMessageTemplate, deepLink1) + titleDisbursement := "You have a payment waiting for you from " + walletDeepLink1.OrganizationName messageDispatcherMock. On("SendMessage", mock.Anything, message.Message{ ToPhoneNumber: receiver1.PhoneNumber, ToEmail: receiver1.Email, Message: contentDisbursement, + Title: titleDisbursement, }, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}). - Return(nil). + Return(message.MessengerTypeTwilioSMS, nil). Once() reqs := []schemas.EventReceiverWalletInvitationData{ @@ -1027,7 +1043,7 @@ func Test_SendReceiverWalletInviteService(t *testing.T) { assert.Equal(t, wallet1.ID, msg.WalletID) assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID) assert.Equal(t, data.SuccessMessageStatus, msg.Status) - assert.Empty(t, msg.TitleEncrypted) + assert.Equal(t, titleDisbursement, msg.TitleEncrypted) assert.Equal(t, contentDisbursement, msg.TextEncrypted) assert.Len(t, msg.StatusHistory, 2) assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status) @@ -1096,7 +1112,7 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitation(t *testing.T) { require.Len(t, entries, 1) assert.Equal( t, - "the invitation message was not resent to the receiver because the maximum number of message resend attempts has been reached: Contact: +12...789 - Receiver ID receiver-ID - Wallet ID wallet-ID - Total Invitation resent 3 - Maximum attempts 3", + "the invitation message was not resent to the receiver because the maximum number of message resend attempts has been reached: Receiver ID receiver-ID - Wallet ID wallet-ID - Total Invitation resent 3 - Maximum attempts 3", entries[0].Message, ) }) @@ -1129,7 +1145,7 @@ func Test_SendReceiverWalletInviteService_shouldSendInvitation(t *testing.T) { assert.Equal( t, fmt.Sprintf( - "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Contact: +12...789 - Receiver ID receiver-ID - Wallet ID wallet-ID - Last Invitation Sent At %s - Receiver Invitation Resend Interval 2 day(s)", + "the invitation message was not automatically resent to the receiver because the receiver is not in the resend period: Receiver ID receiver-ID - Wallet ID wallet-ID - Last Invitation Sent At %s - Receiver Invitation Resend Interval 2 day(s)", invitationSentAt.Format(time.RFC1123), ), entries[0].Message, From f6df71a36e1778ebeae69d145f038a366d24f640 Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Mon, 16 Sep 2024 11:59:21 -0700 Subject: [PATCH 28/75] SDP-1318 Update /wallet-registration/verification to accommodate different contact info fields (#416) --- ...> verify_receiver_registration_handler.go} | 32 +++--- ...ify_receiver_registration_handler_test.go} | 47 +++++--- .../receiver_registration_validator.go | 19 +++- .../receiver_registration_validator_test.go | 102 +++++++++++------- 4 files changed, 128 insertions(+), 72 deletions(-) rename internal/serve/httphandler/{verifiy_receiver_registration_handler.go => verify_receiver_registration_handler.go} (92%) rename internal/serve/httphandler/{verifiy_receiver_registration_handler_test.go => verify_receiver_registration_handler_test.go} (97%) diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler.go b/internal/serve/httphandler/verify_receiver_registration_handler.go similarity index 92% rename from internal/serve/httphandler/verifiy_receiver_registration_handler.go rename to internal/serve/httphandler/verify_receiver_registration_handler.go index 1ed600920..8efba221a 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler.go +++ b/internal/serve/httphandler/verify_receiver_registration_handler.go @@ -116,16 +116,14 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( receiverRegistrationRequest data.ReceiverRegistrationRequest, ) error { now := time.Now() - // TODO: SDP-1318 - Replace with correct contact depending on receiver choice. - truncatedPhoneNumber := utils.TruncateString(receiver.PhoneNumber, 3) // STEP 1: find the receiverVerification entry that matches the pair [receiverID, verificationType] receiverVerifications, err := v.Models.ReceiverVerification.GetByReceiverIDsAndVerificationField(ctx, dbTx, []string{receiver.ID}, receiverRegistrationRequest.VerificationField) if err != nil { - return fmt.Errorf("error retrieving receiver verification for verification type %s: %w", receiverRegistrationRequest.VerificationField, err) + return fmt.Errorf("retrieving receiver verification for verification type %s: %w", receiverRegistrationRequest.VerificationField, err) } if len(receiverVerifications) == 0 { - err = fmt.Errorf("%s not found for receiver with phone number %s", receiverRegistrationRequest.VerificationField, truncatedPhoneNumber) + err = fmt.Errorf("verification of type %s not found for receiver id %s", receiverRegistrationRequest.VerificationField, receiver.ID) return &ErrorInformationNotFound{cause: err} } if len(receiverVerifications) > 1 { @@ -136,7 +134,7 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( // STEP 2: check if the number of attempts to confirm the verification value has already exceeded the max value if v.Models.ReceiverVerification.ExceededAttempts(receiverVerification.Attempts) { // TODO: the application currently can't recover from a max attempts exceeded error. - err = fmt.Errorf("the number of attempts to confirm the verification value exceededs the max attempts") + err = fmt.Errorf("the number of attempts to confirm the verification value exceeded the max attempts") return &ErrorVerificationAttemptsExceeded{cause: err} } @@ -156,7 +154,7 @@ func (v VerifyReceiverRegistrationHandler) processReceiverVerificationPII( } if !data.CompareVerificationValue(receiverVerification.HashedValue, receiverRegistrationRequest.VerificationValue) { - baseErrMsg := fmt.Sprintf("%s value does not match for user with phone number %s", receiverRegistrationRequest.VerificationField, truncatedPhoneNumber) + baseErrMsg := fmt.Sprintf("%s value does not match for receiver with id %s", receiverRegistrationRequest.VerificationField, receiver.ID) // update the receiver verification with the confirmation that the value was checked rvu.Attempts = utils.IntPtr(receiverVerification.Attempts + 1) rvu.FailedAt = &now @@ -278,19 +276,29 @@ func (v VerifyReceiverRegistrationHandler) VerifyReceiverRegistration(w http.Res return } - truncatedPhoneNumber := utils.TruncateString(receiverRegistrationRequest.PhoneNumber, 3) + var contactInfo string + if receiverRegistrationRequest.PhoneNumber != "" { + contactInfo = receiverRegistrationRequest.PhoneNumber + } else if receiverRegistrationRequest.Email != "" { + contactInfo = receiverRegistrationRequest.Email + } else { + httperror.InternalError(ctx, "Unexpected contact info", nil, nil).Render(w) + return + } + + truncatedContactInfo := utils.TruncateString(contactInfo, 3) opts := db.TransactionOptions{ DBConnectionPool: v.Models.DBConnectionPool, AtomicFunctionWithPostCommit: func(dbTx db.DBTransaction) (postCommitFn db.PostCommitFunction, err error) { // STEP 2: find the receivers with the given phone number - receivers, err := v.Models.Receiver.GetByContacts(ctx, dbTx, receiverRegistrationRequest.PhoneNumber) + receivers, err := v.Models.Receiver.GetByContacts(ctx, dbTx, contactInfo) if err != nil { - err = fmt.Errorf("error retrieving receiver with phone number %s: %w", truncatedPhoneNumber, err) + err = fmt.Errorf("retrieving receiver with contact info %s: %w", truncatedContactInfo, err) return nil, err } if len(receivers) == 0 { - err = fmt.Errorf("receiver with phone number %s not found in our server", truncatedPhoneNumber) + err = fmt.Errorf("receiver with contact info %s not found in our server", truncatedContactInfo) return nil, &ErrorInformationNotFound{cause: err} } @@ -298,13 +306,13 @@ func (v VerifyReceiverRegistrationHandler) VerifyReceiverRegistration(w http.Res receiver := receivers[0] err = v.processReceiverVerificationPII(ctx, dbTx, *receiver, receiverRegistrationRequest) if err != nil { - return nil, fmt.Errorf("processing receiver verification entry for receiver with phone number %s: %w", truncatedPhoneNumber, err) + return nil, fmt.Errorf("processing receiver verification entry for receiver with contact info %s: %w", truncatedContactInfo, err) } // STEP 4: process OTP receiverWallet, wasAlreadyRegistered, err := v.processReceiverWalletOTP(ctx, dbTx, *sep24Claims, *receiver, receiverRegistrationRequest.OTP) if err != nil { - return nil, fmt.Errorf("processing OTP for receiver with phone number %s: %w", truncatedPhoneNumber, err) + return nil, fmt.Errorf("processing OTP for receiver with contact info %s: %w", truncatedContactInfo, err) } // STEP 5: build event message to trigger a transaction in the TSS diff --git a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go b/internal/serve/httphandler/verify_receiver_registration_handler_test.go similarity index 97% rename from internal/serve/httphandler/verifiy_receiver_registration_handler_test.go rename to internal/serve/httphandler/verify_receiver_registration_handler_test.go index 135ac59a8..2e67b56c3 100644 --- a/internal/serve/httphandler/verifiy_receiver_registration_handler_test.go +++ b/internal/serve/httphandler/verify_receiver_registration_handler_test.go @@ -243,7 +243,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }, - wantErrContains: "DATE_OF_BIRTH not found for receiver with phone number +38...333", + wantErrContains: "DATE_OF_BIRTH not found for receiver id " + receiverMissingReceiverVerification.ID, }, { name: "returns an error if the receiver does not have any receiverVerification row with the given verification type (YEAR_MONTH)", @@ -253,7 +253,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te VerificationField: data.VerificationTypeYearMonth, VerificationValue: "1999-12", }, - wantErrContains: "YEAR_MONTH not found for receiver with phone number +38...555", + wantErrContains: "YEAR_MONTH not found for receiver id " + receiver.ID, }, { name: "returns an error if the receiver does not have any receiverVerification row with the given verification type (NATIONAL_ID_NUMBER)", @@ -263,7 +263,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te VerificationField: data.VerificationTypeNationalID, VerificationValue: "123456", }, - wantErrContains: "NATIONAL_ID_NUMBER not found for receiver with phone number +38...555", + wantErrContains: "NATIONAL_ID_NUMBER not found for receiver id " + receiver.ID, }, { name: "returns an error if the receiver has exceeded their max attempts to confirm the verification value", @@ -273,7 +273,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }, - wantErrContains: "the number of attempts to confirm the verification value exceededs the max attempts", + wantErrContains: "the number of attempts to confirm the verification value exceeded the max attempts", }, { name: "returns an error if the varification value provided in the payload is different from the DB one", @@ -284,7 +284,7 @@ func Test_VerifyReceiverRegistrationHandler_processReceiverVerificationPII(t *te VerificationValue: "1990-11-11", // <--- different from the DB one (1990-01-01) }, shouldAssertAttemptsCount: true, - wantErrContains: "DATE_OF_BIRTH value does not match for user with phone number +38...555", + wantErrContains: "DATE_OF_BIRTH value does not match for receiver with id " + receiver.ID, }, { name: "๐ŸŽ‰ successfully process the verification value and updates it accordingly in the DB", @@ -775,15 +775,27 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin } phoneNumber := "+380445555555" - receiverRegistrationRequest := data.ReceiverRegistrationRequest{ + receiverRegistrationRequestWithPhone := data.ReceiverRegistrationRequest{ PhoneNumber: phoneNumber, OTP: "123456", VerificationValue: "1990-01-01", VerificationField: "date_of_birth", ReCAPTCHAToken: "token", } - reqBody, err := json.Marshal(receiverRegistrationRequest) + reqBody, err := json.Marshal(receiverRegistrationRequestWithPhone) require.NoError(t, err) + + email := "test@stellar.org" + receiverRegistrationRequestWithEmail := data.ReceiverRegistrationRequest{ + Email: email, + OTP: "123456", + VerificationValue: "1990-01-01", + VerificationField: "date_of_birth", + ReCAPTCHAToken: "token", + } + reqBodyEmail, err := json.Marshal(receiverRegistrationRequestWithEmail) + require.NoError(t, err) + r := chi.NewRouter() t.Run("returns an error when validate() fails - testing case where a SEP24 claims are missing from the context", func(t *testing.T) { @@ -844,7 +856,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin assert.JSONEq(t, wantBody, string(respBody)) // validate logs - require.Contains(t, buf.String(), "receiver with phone number +38...555 not found in our server") + require.Contains(t, buf.String(), "receiver with contact info +38...555 not found in our server") }) t.Run("returns an error when processReceiverVerificationPII() fails - testing case where no receiverVerification is found", func(t *testing.T) { @@ -861,7 +873,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin // update database with the entries needed defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) - _ = data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) + receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) // set the logger to a buffer so we can check the error message buf := new(strings.Builder) @@ -884,7 +896,8 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin assert.JSONEq(t, wantBody, string(respBody)) // validate logs - require.Contains(t, buf.String(), "processing receiver verification entry for receiver with phone number +38...555: DATE_OF_BIRTH not found for receiver with phone number +38...555") + expectedErr := `processing receiver verification entry for receiver with contact info +38...555: verification of type %s not found for receiver id %s` + require.Contains(t, buf.String(), fmt.Sprintf(expectedErr, data.VerificationTypeDateOfBirth, receiver.ID)) }) t.Run("returns an error when processReceiverVerificationPII() fails - testing case where maximum number of verification attempts exceeded", func(t *testing.T) { @@ -934,7 +947,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin respBody, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - expectedError := "the number of attempts to confirm the verification value exceededs the max attempts" + expectedError := "the number of attempts to confirm the verification value exceeded the max attempts" wantBody := fmt.Sprintf(`{"error": "%s"}`, expectedError) assert.JSONEq(t, wantBody, string(respBody)) @@ -985,7 +998,7 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin assert.JSONEq(t, wantBody, string(respBody)) // validate logs - wantErrContains := fmt.Sprintf("processing OTP for receiver with phone number +38...555: receiver wallet not found for receiverID=%s and clientDomain=home.page", receiver.ID) + wantErrContains := fmt.Sprintf("processing OTP for receiver with contact info +38...555: receiver wallet not found for receiverID=%s and clientDomain=home.page", receiver.ID) require.Contains(t, buf.String(), wantErrContains) }) @@ -1202,19 +1215,19 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin defer data.DeleteAllReceiversFixtures(t, ctx, dbConnectionPool) defer data.DeleteAllReceiverVerificationFixtures(t, ctx, dbConnectionPool) defer data.DeleteAllReceiverWalletsFixtures(t, ctx, dbConnectionPool) - receiver := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{PhoneNumber: phoneNumber}) + receiver := data.InsertReceiverFixture(t, ctx, dbConnectionPool, &data.ReceiverInsert{Email: &email}) _ = data.CreateReceiverVerificationFixture(t, ctx, dbConnectionPool, data.ReceiverVerificationInsert{ ReceiverID: receiver.ID, VerificationField: data.VerificationTypeDateOfBirth, VerificationValue: "1990-01-01", }) receiverWallet := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet.ID, data.ReadyReceiversWalletStatus) - _, err := models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, "+380445555555", wallet.SEP10ClientDomain, "123456") + _, err := models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, email, wallet.SEP10ClientDomain, "123456") require.NoError(t, err) // setup router and execute request r.Post("/wallet-registration/verification", handler.VerifyReceiverRegistration) - req, err := http.NewRequest("POST", "/wallet-registration/verification", strings.NewReader(string(reqBody))) + req, err := http.NewRequest("POST", "/wallet-registration/verification", strings.NewReader(string(reqBodyEmail))) require.NoError(t, err) req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, &sep24Claims)) rr := httptest.NewRecorder() @@ -1258,12 +1271,12 @@ func Test_VerifyReceiverRegistrationHandler_VerifyReceiverRegistration(t *testin // registering Second Wallet receiverWallet2 := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver.ID, wallet2.ID, data.ReadyReceiversWalletStatus) - _, err = models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, "+380445555555", wallet2.SEP10ClientDomain, "123456") + _, err = models.ReceiverWallet.UpdateOTPByReceiverContactInfoAndWalletDomain(ctx, email, wallet2.SEP10ClientDomain, "123456") require.NoError(t, err) sep24Claims.ClientDomainClaim = wallet2.SEP10ClientDomain - req, err = http.NewRequest("POST", "/wallet-registration/verification", strings.NewReader(string(reqBody))) + req, err = http.NewRequest("POST", "/wallet-registration/verification", strings.NewReader(string(reqBodyEmail))) require.NoError(t, err) req = req.WithContext(context.WithValue(req.Context(), anchorplatform.SEP24ClaimsContextKey, &sep24Claims)) rr = httptest.NewRecorder() diff --git a/internal/serve/validators/receiver_registration_validator.go b/internal/serve/validators/receiver_registration_validator.go index 0fdf20367..10ca64a9f 100644 --- a/internal/serve/validators/receiver_registration_validator.go +++ b/internal/serve/validators/receiver_registration_validator.go @@ -23,14 +23,24 @@ func NewReceiverRegistrationValidator() *ReceiverRegistrationValidator { // ValidateReceiver validates if the infos present in the ReceiverRegistrationRequest are valids. func (rv *ReceiverRegistrationValidator) ValidateReceiver(receiverInfo *data.ReceiverRegistrationRequest) { - phone := strings.TrimSpace(receiverInfo.PhoneNumber) + phone := utils.TrimAndLower(receiverInfo.PhoneNumber) + email := utils.TrimAndLower(receiverInfo.Email) otp := strings.TrimSpace(receiverInfo.OTP) verification := strings.TrimSpace(receiverInfo.VerificationValue) verificationField := strings.TrimSpace(string(receiverInfo.VerificationField)) - // validate phone field - rv.CheckError(utils.ValidatePhoneNumber(phone), "phone_number", "invalid phone format. Correct format: +380445555555") - rv.Check(phone != "", "phone_number", "phone cannot be empty") + switch { + case phone == "" && email == "": + rv.Check(false, "phone_number", "phone_number or email is required") + rv.Check(false, "email", "phone_number or email is required") + case phone != "" && email != "": + rv.Check(false, "phone_number", "phone_number and email cannot be both provided") + rv.Check(false, "email", "phone_number and email cannot be both provided") + case phone != "": + rv.CheckError(utils.ValidatePhoneNumber(phone), "phone_number", "") + case email != "": + rv.CheckError(utils.ValidateEmail(email), "email", "") + } // validate otp field rv.CheckError(utils.ValidateOTP(otp), "otp", "invalid otp format. Needs to be a 6 digit value") @@ -52,6 +62,7 @@ func (rv *ReceiverRegistrationValidator) ValidateReceiver(receiverInfo *data.Rec } receiverInfo.PhoneNumber = phone + receiverInfo.Email = email receiverInfo.OTP = otp receiverInfo.VerificationValue = verification receiverInfo.VerificationField = vf diff --git a/internal/serve/validators/receiver_registration_validator_test.go b/internal/serve/validators/receiver_registration_validator_test.go index 0376c16e7..84c7d6817 100644 --- a/internal/serve/validators/receiver_registration_validator_test.go +++ b/internal/serve/validators/receiver_registration_validator_test.go @@ -10,12 +10,10 @@ import ( func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { type testCase struct { - name string - receiverInfo data.ReceiverRegistrationRequest - expectedErrorLen int - expectedErrorMsg string - expectedErrorKey string - expectedReceiver data.ReceiverRegistrationRequest + name string + receiverInfo data.ReceiverRegistrationRequest + expectedReceiver data.ReceiverRegistrationRequest + expectedValidationErrors map[string]interface{} } testCases := []testCase{ @@ -27,21 +25,48 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "1990-01-01", VerificationField: data.VerificationTypeDateOfBirth, }, - expectedErrorLen: 1, - expectedErrorMsg: "invalid phone format. Correct format: +380445555555", - expectedErrorKey: "phone_number", + expectedValidationErrors: map[string]interface{}{ + "phone_number": "the provided phone number is not a valid E.164 number", + }, }, { - name: "error if phone number is empty", + name: "error if email is invalid", + receiverInfo: data.ReceiverRegistrationRequest{ + Email: "invalid", + OTP: "123456", + VerificationValue: "1990-01-01", + VerificationField: data.VerificationTypeDateOfBirth, + }, + expectedValidationErrors: map[string]interface{}{ + "email": "the provided email is not valid", + }, + }, + { + name: "error if phone number and email are empty", receiverInfo: data.ReceiverRegistrationRequest{ PhoneNumber: "", OTP: "123456", VerificationValue: "1990-01-01", VerificationField: data.VerificationTypeDateOfBirth, }, - expectedErrorLen: 1, - expectedErrorMsg: "phone cannot be empty", - expectedErrorKey: "phone_number", + expectedValidationErrors: map[string]interface{}{ + "phone_number": "phone_number or email is required", + "email": "phone_number or email is required", + }, + }, + { + name: "error if phone number and email are provided", + receiverInfo: data.ReceiverRegistrationRequest{ + Email: "test@stellar.com", + PhoneNumber: "+380445555555", + OTP: "123456", + VerificationValue: "1990-01-01", + VerificationField: data.VerificationTypeDateOfBirth, + }, + expectedValidationErrors: map[string]interface{}{ + "phone_number": "phone_number and email cannot be both provided", + "email": "phone_number and email cannot be both provided", + }, }, { name: "error if OTP is invalid", @@ -51,9 +76,9 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "1990-01-01", VerificationField: data.VerificationTypeDateOfBirth, }, - expectedErrorLen: 1, - expectedErrorMsg: "invalid otp format. Needs to be a 6 digit value", - expectedErrorKey: "otp", + expectedValidationErrors: map[string]interface{}{ + "otp": "invalid otp format. Needs to be a 6 digit value", + }, }, { name: "error if verification type is invalid", @@ -63,9 +88,9 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "1990-01-01", VerificationField: "mock_type", }, - expectedErrorLen: 1, - expectedErrorMsg: "invalid parameter. valid values are: [DATE_OF_BIRTH YEAR_MONTH PIN NATIONAL_ID_NUMBER]", - expectedErrorKey: "verification_field", + expectedValidationErrors: map[string]interface{}{ + "verification_field": "invalid parameter. valid values are: [DATE_OF_BIRTH YEAR_MONTH PIN NATIONAL_ID_NUMBER]", + }, }, { name: "error if verification[DATE_OF_BIRTH] is invalid", @@ -75,9 +100,9 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "90/01/01", VerificationField: data.VerificationTypeDateOfBirth, }, - expectedErrorLen: 1, - expectedErrorMsg: "invalid date of birth format. Correct format: 1990-01-30", - expectedErrorKey: "verification", + expectedValidationErrors: map[string]interface{}{ + "verification": "invalid date of birth format. Correct format: 1990-01-30", + }, }, { name: "error if verification[YEAR_MONTH] is invalid", @@ -87,9 +112,9 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "90/12", VerificationField: data.VerificationTypeYearMonth, }, - expectedErrorLen: 1, - expectedErrorMsg: "invalid year/month format. Correct format: 1990-12", - expectedErrorKey: "verification", + expectedValidationErrors: map[string]interface{}{ + "verification": "invalid year/month format. Correct format: 1990-12", + }, }, { name: "error if verification[PIN] is invalid", @@ -99,9 +124,9 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "ABCDE1234", VerificationField: data.VerificationTypePin, }, - expectedErrorLen: 1, - expectedErrorMsg: "invalid pin length. Cannot have less than 4 or more than 8 characters in pin", - expectedErrorKey: "verification", + expectedValidationErrors: map[string]interface{}{ + "verification": "invalid pin length. Cannot have less than 4 or more than 8 characters in pin", + }, }, { name: "error if verification[NATIONAL_ID_NUMBER] is invalid", @@ -111,9 +136,9 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "6UZMB56FWTKV4U0PJ21TBR6VOQVYSGIMZG2HW2S0L7EK5K83W78XXXXX", VerificationField: data.VerificationTypeNationalID, }, - expectedErrorLen: 1, - expectedErrorMsg: "invalid national id. Cannot have more than 50 characters in national id", - expectedErrorKey: "verification", + expectedValidationErrors: map[string]interface{}{ + "verification": "invalid national id. Cannot have more than 50 characters in national id", + }, }, { name: "๐ŸŽ‰ successfully validates receiver values [DATE_OF_BIRTH]", @@ -123,7 +148,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "1990-01-01 ", VerificationField: "date_of_birth", }, - expectedErrorLen: 0, + expectedValidationErrors: map[string]interface{}{}, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", @@ -139,7 +164,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "1990-12 ", VerificationField: "year_month", }, - expectedErrorLen: 0, + expectedValidationErrors: map[string]interface{}{}, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", @@ -155,7 +180,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: "1234 ", VerificationField: "pin", }, - expectedErrorLen: 0, + expectedValidationErrors: map[string]interface{}{}, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", @@ -171,7 +196,7 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { VerificationValue: " NATIONALIDNUMBER123", VerificationField: "national_id_number", }, - expectedErrorLen: 0, + expectedValidationErrors: map[string]interface{}{}, expectedReceiver: data.ReceiverRegistrationRequest{ PhoneNumber: "+380445555555", OTP: "123456", @@ -186,11 +211,10 @@ func Test_ReceiverRegistrationValidator_ValidateReceiver(t *testing.T) { validator := NewReceiverRegistrationValidator() validator.ValidateReceiver(&tc.receiverInfo) - assert.Equal(t, tc.expectedErrorLen, len(validator.Errors)) - - if tc.expectedErrorLen > 0 { - assert.Equal(t, tc.expectedErrorMsg, validator.Errors[tc.expectedErrorKey]) + if len(tc.expectedValidationErrors) > 0 { + assert.Equal(t, tc.expectedValidationErrors, validator.Errors) } else { + assert.Equal(t, tc.expectedReceiver.Email, tc.receiverInfo.Email) assert.Equal(t, tc.expectedReceiver.PhoneNumber, tc.receiverInfo.PhoneNumber) assert.Equal(t, tc.expectedReceiver.OTP, tc.receiverInfo.OTP) assert.Equal(t, tc.expectedReceiver.VerificationValue, tc.receiverInfo.VerificationValue) From 62853091149d5ec8d16ef58a4867942903dbcb0c Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Tue, 17 Sep 2024 08:36:11 -0700 Subject: [PATCH 29/75] SDP-1343 HTML Injection Vulnerability (#419) --- internal/htmltemplate/htmltemplate.go | 2 +- internal/htmltemplate/htmltemplate_test.go | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/htmltemplate/htmltemplate.go b/internal/htmltemplate/htmltemplate.go index f6589573c..c3170918b 100644 --- a/internal/htmltemplate/htmltemplate.go +++ b/internal/htmltemplate/htmltemplate.go @@ -4,7 +4,7 @@ import ( "bytes" "embed" "fmt" - "text/template" + "html/template" ) //go:embed tmpl/*.tmpl diff --git a/internal/htmltemplate/htmltemplate_test.go b/internal/htmltemplate/htmltemplate_test.go index 7fe0303ff..667e33483 100644 --- a/internal/htmltemplate/htmltemplate_test.go +++ b/internal/htmltemplate/htmltemplate_test.go @@ -14,7 +14,7 @@ func Test_ExecuteHTMLTemplate(t *testing.T) { var inputData interface{} templateStr, err := ExecuteHTMLTemplate("non-existing-file.html", inputData) require.Empty(t, templateStr) - require.EqualError(t, err, `executing html template: template: no template "non-existing-file.html" associated with template "empty_body.tmpl"`) + require.EqualError(t, err, `executing html template: html/template: "non-existing-file.html" is undefined`) // handle invalid struct body inputData = struct { @@ -67,6 +67,24 @@ func Test_ExecuteHTMLTemplateForInvitationMessage(t *testing.T) { assert.Contains(t, content, "Organization Name") } +func Test_ExecuteHTMLTemplateForInvitationMessage_HTMLInjectionAttack(t *testing.T) { + forgotPasswordLink := "https://sdp.com/forgot-password" + + data := InvitationMessageTemplate{ + FirstName: "First", + Role: "developer", + ForgotPasswordLink: forgotPasswordLink, + OrganizationName: "Redeem funds", + } + content, err := ExecuteHTMLTemplateForInvitationMessage(data) + require.NoError(t, err) + + assert.Contains(t, content, "Hello, First!") + assert.Contains(t, content, "as a developer.") + assert.Contains(t, content, forgotPasswordLink) + assert.Contains(t, content, "<a href='evil.com'>Redeem funds</a>") +} + func Test_ExecuteHTMLTemplateForForgotPasswordMessage(t *testing.T) { data := ForgotPasswordMessageTemplate{ ResetToken: "resetToken", From 3c64059046e556754d14bb0c88740e0071ed3d25 Mon Sep 17 00:00:00 2001 From: Marwen Abid Date: Tue, 17 Sep 2024 12:04:18 -0700 Subject: [PATCH 30/75] [SDP-1306] Update the `receiver_registered_successfully.tmpl` HTML template to display the contact info (#418) * SDP-1306 Fix payments test order * SDP-1306 Update the receiver_registered_successfully.tmpl HTML template to display the contact info that was used to OTP * SDP-1306 Change `otp-confirmed-by` to `otp-confirmed-with` --- ...r-wallet-add-otp-confirmed-with-column.sql | 7 ++++ internal/data/payments_test.go | 38 +++++++++++++++---- internal/data/receivers_wallet.go | 35 +++++++++-------- internal/data/receivers_wallet_test.go | 5 ++- .../receiver_registered_successfully.tmpl | 4 ++ .../httphandler/payments_handler_test.go | 3 +- .../httphandler/receiver_registration.go | 17 +++++---- .../verify_receiver_registration_handler.go | 4 +- ...rify_receiver_registration_handler_test.go | 12 ++++-- 9 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 db/migrations/sdp-migrations/2024-09-16.0-receiver-wallet-add-otp-confirmed-with-column.sql diff --git a/db/migrations/sdp-migrations/2024-09-16.0-receiver-wallet-add-otp-confirmed-with-column.sql b/db/migrations/sdp-migrations/2024-09-16.0-receiver-wallet-add-otp-confirmed-with-column.sql new file mode 100644 index 000000000..775016cdf --- /dev/null +++ b/db/migrations/sdp-migrations/2024-09-16.0-receiver-wallet-add-otp-confirmed-with-column.sql @@ -0,0 +1,7 @@ +-- +migrate Up +ALTER TABLE receiver_wallets + ADD COLUMN otp_confirmed_with VARCHAR(256) NULL; + +-- +migrate Down +ALTER TABLE receiver_wallets + DROP COLUMN otp_confirmed_with; \ No newline at end of file diff --git a/internal/data/payments_test.go b/internal/data/payments_test.go index 0efe50ef1..193bbce89 100644 --- a/internal/data/payments_test.go +++ b/internal/data/payments_test.go @@ -283,6 +283,7 @@ func Test_PaymentModelGetAll(t *testing.T) { Disbursement: disbursement1, Asset: *asset, ReceiverWallet: receiverWallet, + UpdatedAt: time.Date(2022, 3, 21, 23, 40, 20, 1431, time.UTC), }) stellarTransactionID, err = utils.RandomString(64) @@ -305,27 +306,41 @@ func Test_PaymentModelGetAll(t *testing.T) { Disbursement: disbursement2, Asset: *asset, ReceiverWallet: receiverWallet, + UpdatedAt: time.Date(2023, 3, 21, 23, 40, 20, 1431, time.UTC), }) t.Run("returns payments successfully", func(t *testing.T) { - actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{}, dbConnectionPool) + params := QueryParams{SortBy: DefaultPaymentSortField, SortOrder: DefaultPaymentSortOrder} + actualPayments, err := paymentModel.GetAll(ctx, ¶ms, dbConnectionPool) require.NoError(t, err) assert.Equal(t, 2, len(actualPayments)) assert.Equal(t, []Payment{*expectedPayment2, *expectedPayment1}, actualPayments) }) t.Run("returns payments successfully with limit", func(t *testing.T) { - actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{Page: 1, PageLimit: 1}, dbConnectionPool) + params := QueryParams{ + SortBy: DefaultPaymentSortField, + SortOrder: DefaultPaymentSortOrder, + Page: 1, + PageLimit: 1, + } + actualPayments, err := paymentModel.GetAll(ctx, ¶ms, dbConnectionPool) require.NoError(t, err) assert.Equal(t, 1, len(actualPayments)) - assert.Equal(t, []Payment{*expectedPayment1}, actualPayments) + assert.Equal(t, []Payment{*expectedPayment2}, actualPayments) }) t.Run("returns payments successfully with offset", func(t *testing.T) { - actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{Page: 2, PageLimit: 1}, dbConnectionPool) + params := QueryParams{ + Page: 2, + PageLimit: 1, + SortBy: DefaultPaymentSortField, + SortOrder: DefaultPaymentSortOrder, + } + actualPayments, err := paymentModel.GetAll(ctx, ¶ms, dbConnectionPool) require.NoError(t, err) assert.Equal(t, 1, len(actualPayments)) - assert.Equal(t, []Payment{*expectedPayment2}, actualPayments) + assert.Equal(t, []Payment{*expectedPayment1}, actualPayments) }) t.Run("returns payments successfully with created at order", func(t *testing.T) { @@ -361,10 +376,15 @@ func Test_PaymentModelGetAll(t *testing.T) { PendingPaymentStatus, }, } - actualPayments, err := paymentModel.GetAll(ctx, &QueryParams{Filters: filters}, dbConnectionPool) + queryParams := QueryParams{ + Filters: filters, + SortBy: DefaultPaymentSortField, + SortOrder: DefaultPaymentSortOrder, + } + actualPayments, err := paymentModel.GetAll(ctx, &queryParams, dbConnectionPool) require.NoError(t, err) assert.Equal(t, 2, len(actualPayments)) - assert.Equal(t, []Payment{*expectedPayment1, *expectedPayment2}, actualPayments) + assert.Equal(t, []Payment{*expectedPayment2, *expectedPayment1}, actualPayments) }) t.Run("should not return duplicated entries when receiver are in more than one disbursements with different wallets", func(t *testing.T) { @@ -410,6 +430,7 @@ func Test_PaymentModelGetAll(t *testing.T) { Disbursement: disbursement1, Asset: *usdc, ReceiverWallet: receiverDemoWallet, + UpdatedAt: time.Date(2023, 3, 21, 23, 40, 20, 1431, time.UTC), }) vibrantWalletPayment := CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &Payment{ @@ -418,12 +439,15 @@ func Test_PaymentModelGetAll(t *testing.T) { Disbursement: disbursement2, Asset: *usdc, ReceiverWallet: receiverVibrantWallet, + UpdatedAt: time.Date(2022, 3, 21, 23, 40, 20, 1431, time.UTC), }) payments, err := models.Payment.GetAll(ctx, &QueryParams{ Filters: map[FilterKey]interface{}{ FilterKeyReceiverID: receiver.ID, }, + SortBy: DefaultPaymentSortField, + SortOrder: DefaultPaymentSortOrder, }, dbConnectionPool) require.NoError(t, err) diff --git a/internal/data/receivers_wallet.go b/internal/data/receivers_wallet.go index 73ceeb27f..9c8b8aecc 100644 --- a/internal/data/receivers_wallet.go +++ b/internal/data/receivers_wallet.go @@ -63,19 +63,20 @@ func (rwsh *ReceiversWalletStatusHistory) Scan(src interface{}) error { var _ sql.Scanner = (*ReceiversWalletStatusHistory)(nil) type ReceiverWallet struct { - ID string `json:"id" db:"id"` - Receiver Receiver `json:"receiver" db:"receiver"` - Wallet Wallet `json:"wallet" db:"wallet"` - StellarAddress string `json:"stellar_address,omitempty" db:"stellar_address"` - StellarMemo string `json:"stellar_memo,omitempty" db:"stellar_memo"` - StellarMemoType string `json:"stellar_memo_type,omitempty" db:"stellar_memo_type"` - Status ReceiversWalletStatus `json:"status" db:"status"` - StatusHistory ReceiversWalletStatusHistory `json:"status_history,omitempty" db:"status_history"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - OTP string `json:"-" db:"otp"` - OTPCreatedAt *time.Time `json:"-" db:"otp_created_at"` - OTPConfirmedAt *time.Time `json:"otp_confirmed_at,omitempty" db:"otp_confirmed_at"` + ID string `json:"id" db:"id"` + Receiver Receiver `json:"receiver" db:"receiver"` + Wallet Wallet `json:"wallet" db:"wallet"` + StellarAddress string `json:"stellar_address,omitempty" db:"stellar_address"` + StellarMemo string `json:"stellar_memo,omitempty" db:"stellar_memo"` + StellarMemoType string `json:"stellar_memo_type,omitempty" db:"stellar_memo_type"` + Status ReceiversWalletStatus `json:"status" db:"status"` + StatusHistory ReceiversWalletStatusHistory `json:"status_history,omitempty" db:"status_history"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + OTP string `json:"-" db:"otp"` + OTPCreatedAt *time.Time `json:"-" db:"otp_created_at"` + OTPConfirmedAt *time.Time `json:"otp_confirmed_at,omitempty" db:"otp_confirmed_at"` + OTPConfirmedWith string `json:"otp_confirmed_with,omitempty" db:"otp_confirmed_with"` // AnchorPlatformAccountID is the ID of the SEP24 transaction initiated by the Anchor Platform where the receiver wallet was registered. AnchorPlatformTransactionID string `json:"anchor_platform_transaction_id,omitempty" db:"anchor_platform_transaction_id"` AnchorPlatformTransactionSyncedAt *time.Time `json:"anchor_platform_transaction_synced_at,omitempty" db:"anchor_platform_transaction_synced_at"` @@ -359,6 +360,7 @@ func (rw *ReceiverWalletModel) GetByReceiverIDAndWalletDomain(ctx context.Contex 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" @@ -392,8 +394,9 @@ func (rw *ReceiverWalletModel) UpdateReceiverWallet(ctx context.Context, receive stellar_address = $3, stellar_memo = $4, stellar_memo_type = $5, - otp_confirmed_at = $6 - WHERE rw.id = $7 + otp_confirmed_at = $6, + otp_confirmed_with = $7 + WHERE rw.id = $8 ` result, err := sqlExec.ExecContext(ctx, query, @@ -403,6 +406,7 @@ func (rw *ReceiverWalletModel) UpdateReceiverWallet(ctx context.Context, receive 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) @@ -494,6 +498,7 @@ func (rw *ReceiverWalletModel) GetByStellarAccountAndMemo(ctx context.Context, s 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" diff --git a/internal/data/receivers_wallet_test.go b/internal/data/receivers_wallet_test.go index 6ce3da101..5396caa63 100644 --- a/internal/data/receivers_wallet_test.go +++ b/internal/data/receivers_wallet_test.go @@ -553,6 +553,7 @@ func Test_UpdateReceiverWallet(t *testing.T) { receiverWallet.Status = RegisteredReceiversWalletStatus now := time.Now() receiverWallet.OTPConfirmedAt = &now + receiverWallet.OTPConfirmedWith = "test@stellar.org" err := receiverWalletModel.UpdateReceiverWallet(ctx, *receiverWallet, dbConnectionPool) require.NoError(t, err) @@ -565,7 +566,8 @@ func Test_UpdateReceiverWallet(t *testing.T) { rw.stellar_address, rw.stellar_memo, rw.stellar_memo_type, - otp_confirmed_at + otp_confirmed_at, + COALESCE(rw.otp_confirmed_with, '') as otp_confirmed_with FROM receiver_wallets rw WHERE @@ -581,6 +583,7 @@ func Test_UpdateReceiverWallet(t *testing.T) { 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) }) } diff --git a/internal/htmltemplate/tmpl/receiver_registered_successfully.tmpl b/internal/htmltemplate/tmpl/receiver_registered_successfully.tmpl index c3c26ab3e..c09704008 100644 --- a/internal/htmltemplate/tmpl/receiver_registered_successfully.tmpl +++ b/internal/htmltemplate/tmpl/receiver_registered_successfully.tmpl @@ -23,6 +23,10 @@

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,