diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7d24c354..88236d1ec 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@971e284b6050e8a5849b72094c50ab08da042db8 # version v6.1.1 + uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # version v6.2.0 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 553f2c884..096e257e9 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.11.0 + uses: docker/build-push-action@v6.13.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.11.0 + uses: docker/build-push-action@v6.13.0 with: push: true build-args: | diff --git a/CHANGELOG.md b/CHANGELOG.md index ff0cae0ad..b90d7ee95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## [3.4.0](https://github.com/stellar/stellar-disbursement-platform-backend/releases/tag/3.4.0) ([diff](https://github.com/stellar/stellar-disbursement-platform-backend/compare/3.3.0...3.4.0)) + +Release of the Stellar Disbursement Platform `v3.4.0`. This release adds support for `q={term}` query searches in the +`GET /payments` endpoint, and updates the CSV parser to ignore BOM (Byte Order Mark) characters. + +> [!WARNING] +> This version is compatible with the [stellar/stellar-disbursement-platform-frontend] version `3.4.0`. + +### Changed + +- Update the `GET /payments` endpoint to accept `q={term}` query searches. [#530](https://github.com/stellar/stellar-disbursement-platform-backend/pull/530) +- Update the CSV parser to ignore BOM (Byte Order Mark) characters. [#531](https://github.com/stellar/stellar-disbursement-platform-backend/pull/531) + +### Security and Dependencies + +- Bump golang in the all-docker group. [#507](https://github.com/stellar/stellar-disbursement-platform-backend/pull/507) +- Bump the all-actions group. [#514](https://github.com/stellar/stellar-disbursement-platform-backend/pull/514) +- Bump the minor-and-patch group. [#529](https://github.com/stellar/stellar-disbursement-platform-backend/pull/529) + ## [3.3.0](https://github.com/stellar/stellar-disbursement-platform-backend/releases/tag/3.3.0) ([diff](https://github.com/stellar/stellar-disbursement-platform-backend/compare/3.2.0...3.3.0)) Release of the Stellar Disbursement Platform `v3.3.0`. This release adds support to Circle's Transfers API, as an diff --git a/Dockerfile b/Dockerfile index b756c4eed..7e913e9ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # To push: # make docker-push -FROM golang:1.23.4-bullseye AS build +FROM golang:1.23.5-bullseye AS build ARG GIT_COMMIT WORKDIR /src/stellar-disbursement-platform diff --git a/Dockerfile.development b/Dockerfile.development index e3744a5e0..d03396e65 100644 --- a/Dockerfile.development +++ b/Dockerfile.development @@ -1,5 +1,5 @@ # Stage 1: Build the Go application -FROM golang:1.23.4-bullseye AS build +FROM golang:1.23.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.23.4-bullseye AS development +FROM golang:1.23.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 diff --git a/go.list b/go.list index ca5923e7d..78c79f658 100644 --- a/go.list +++ b/go.list @@ -27,7 +27,7 @@ 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/v4 v4.6.0 -github.com/aws/aws-sdk-go v1.55.5 +github.com/aws/aws-sdk-go v1.55.6 github.com/beevik/etree v1.1.0 github.com/beorn7/perks v1.0.1 github.com/bgentry/speakeasy v0.1.0 @@ -47,6 +47,7 @@ github.com/creachadair/mds v0.0.1 github.com/creack/pty v1.1.9 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/denisenkom/go-mssqldb v0.9.0 +github.com/dimchansky/utfbom v1.1.1 github.com/distribution/reference v0.6.0 github.com/djherbis/fscache v0.10.1 github.com/docker/docker v27.3.1+incompatible @@ -159,7 +160,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.8 -github.com/nyaruka/phonenumbers v1.4.4 +github.com/nyaruka/phonenumbers v1.5.0 github.com/olekukonko/tablewriter v0.0.5 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.10 @@ -207,7 +208,7 @@ github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible github.com/stretchr/objx v0.5.2 github.com/stretchr/testify v1.10.0 github.com/subosito/gotenv v1.6.0 -github.com/twilio/twilio-go v1.23.9 +github.com/twilio/twilio-go v1.23.11 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.34.0 diff --git a/go.mod b/go.mod index f1ac5a33f..bf92c2591 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.22.1 require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/avast/retry-go/v4 v4.6.0 - github.com/aws/aws-sdk-go v1.55.5 + github.com/aws/aws-sdk-go v1.55.6 + github.com/dimchansky/utfbom v1.1.1 github.com/getsentry/sentry-go v0.31.1 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/chi/v5 v5.2.0 @@ -18,7 +19,7 @@ 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.4 + github.com/nyaruka/phonenumbers v1.5.0 github.com/prometheus/client_golang v1.20.5 github.com/rs/cors v1.11.1 github.com/rubenv/sql-migrate v1.7.1 @@ -30,7 +31,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stellar/go v0.0.0-20241115082344-969db9917c2d github.com/stretchr/testify v1.10.0 - github.com/twilio/twilio-go v1.23.9 + github.com/twilio/twilio-go v1.23.11 golang.org/x/crypto v0.32.0 golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d golang.org/x/net v0.34.0 diff --git a/go.sum b/go.sum index afc425243..ef7eb80d2 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ 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/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= -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/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/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= @@ -32,6 +32,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -118,8 +120,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.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/nyaruka/phonenumbers v1.4.4 h1:9yo9jLvXD7J4exe7GJATApgTlB+05snF0joMDL1p7nQ= -github.com/nyaruka/phonenumbers v1.4.4/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E= +github.com/nyaruka/phonenumbers v1.5.0 h1:0M+Gd9zl53QC4Nl5z1Yj1O/zPk2XXBUwR/vlzdXSJv4= +github.com/nyaruka/phonenumbers v1.5.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= @@ -200,8 +202,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.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.9 h1:lzeeYez9PFT5aILZusqBf1yOEIlcom3J0CZU32bJkxk= -github.com/twilio/twilio-go v1.23.9/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= +github.com/twilio/twilio-go v1.23.11 h1:Q532m0rgWF1AzzF4Z4ejzTk5XeORWT+zLGzlklSk/iU= +github.com/twilio/twilio-go v1.23.11/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.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= diff --git a/helmchart/sdp/Chart.yaml b/helmchart/sdp/Chart.yaml index 53531f951..cde4a18b0 100644 --- a/helmchart/sdp/Chart.yaml +++ b/helmchart/sdp/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: stellar-disbursement-platform description: A Helm chart for the Stellar Disbursement Platform Backend (A.K.A. `sdp`) version: "3.3.0" -appVersion: "3.3.0" +appVersion: "3.4.0" type: application maintainers: - name: Stellar Development Foundation diff --git a/helmchart/sdp/README.md b/helmchart/sdp/README.md index c765ceb54..6e273a5ab 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`. | `3.3.0` | +| `sdp.image.tag` | Docker image tag for the SDP service. If set, this overrides the default value from `.Chart.AppVersion`. | `3.4.0` | | `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. | `{}` | @@ -291,7 +291,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:3.3.0` | +| `dashboard.image.fullName` | Full name of the Docker image. | `stellar/stellar-disbursement-platform-frontend:3.4.0` | | `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/values.yaml b/helmchart/sdp/values.yaml index 59e4c08a0..adcfe5c1f 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: "3.3.0" + tag: "3.4.0" ## @extra sdp.deployment Configuration related to the deployment of the SDP service. ## @param sdp.deployment.annotations Annotations to be added to the deployment. @@ -536,7 +536,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:3.3.0 + fullName: stellar/stellar-disbursement-platform-frontend:3.4.0 pullPolicy: Always ## @extra dashboard.deployment Configuration related to the deployment of the Dashboard. diff --git a/internal/data/payments.go b/internal/data/payments.go index 14ff63280..225bb7fd7 100644 --- a/internal/data/payments.go +++ b/internal/data/payments.go @@ -248,7 +248,7 @@ func (p *PaymentModel) Count(ctx context.Context, queryParams *QueryParams, sqlE payments p JOIN disbursements d on p.disbursement_id = d.id JOIN assets a on p.asset_id = a.id - JOIN wallets w on d.wallet_id = w.id + JOIN wallets w on d.wallet_id = w.id JOIN receiver_wallets rw on rw.receiver_id = p.receiver_id AND rw.wallet_id = w.id ` @@ -622,6 +622,11 @@ func (p *PaymentModel) GetByIDs(ctx context.Context, sqlExec db.SQLExecuter, pay // newPaymentQuery generates the full query and parameters for a payment search query func newPaymentQuery(baseQuery string, queryParams *QueryParams, sqlExec db.SQLExecuter, queryType QueryType) (string, []interface{}) { qb := NewQueryBuilder(baseQuery) + if queryParams.Query != "" { + q := "%" + queryParams.Query + "%" + qb.AddCondition("(p.id ILIKE ? OR p.external_payment_id ILIKE ? OR rw.stellar_address ILIKE ? OR d.name ILIKE ?)", q, q, q, q) + } + if queryParams.Filters[FilterKeyStatus] != nil { if statusSlice, ok := queryParams.Filters[FilterKeyStatus].([]PaymentStatus); ok { if len(statusSlice) > 0 { diff --git a/internal/data/payments_test.go b/internal/data/payments_test.go index aaaa3f986..48d62050a 100644 --- a/internal/data/payments_test.go +++ b/internal/data/payments_test.go @@ -620,6 +620,16 @@ func Test_PaymentNewPaymentQuery(t *testing.T) { expectedQuery: "SELECT * FROM payments p", expectedParams: []interface{}{}, }, + { + name: "build payment query with a query search", + baseQuery: "SELECT * FROM payments p", + queryParams: QueryParams{ + Query: "foo-bar", + }, + queryType: QueryTypeSelectAll, + expectedQuery: "SELECT * FROM payments p WHERE 1=1 AND (p.id ILIKE $1 OR p.external_payment_id ILIKE $2 OR rw.stellar_address ILIKE $3 OR d.name ILIKE $4)", + expectedParams: []interface{}{"%foo-bar%", "%foo-bar%", "%foo-bar%", "%foo-bar%"}, + }, { name: "build payment query with status filter", baseQuery: "SELECT * FROM payments p", diff --git a/internal/serve/httphandler/disbursement_handler.go b/internal/serve/httphandler/disbursement_handler.go index 02e53e8b9..d6f2ef729 100644 --- a/internal/serve/httphandler/disbursement_handler.go +++ b/internal/serve/httphandler/disbursement_handler.go @@ -14,6 +14,7 @@ import ( "slices" "time" + "github.com/dimchansky/utfbom" "github.com/go-chi/chi/v5" "github.com/gocarina/gocsv" "github.com/stellar/go/support/log" @@ -586,16 +587,23 @@ func parseInstructionsFromCSV(ctx context.Context, reader io.Reader, contactType // 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() + const ( + phoneHeader = "phone" + emailHeader = "email" + walletAddressHeader = "walletAddress" + verificationHeader = "verification" + ) + + headers, err := csv.NewReader(utfbom.SkipOnly(file)).Read() if err != nil { return fmt.Errorf("reading csv headers: %w", err) } hasHeaders := map[string]bool{ - "phone": false, - "email": false, - "walletAddress": false, - "verification": false, + phoneHeader: false, + emailHeader: false, + walletAddressHeader: false, + verificationHeader: false, } // Populate header presence map @@ -613,20 +621,20 @@ func validateCSVHeaders(file io.Reader, registrationContactType data.Registratio rules := map[data.RegistrationContactType]headerRules{ data.RegistrationContactTypePhone: { - required: []string{"phone", "verification"}, - disallowed: []string{"email", "walletAddress"}, + required: []string{phoneHeader, verificationHeader}, + disallowed: []string{emailHeader, walletAddressHeader}, }, data.RegistrationContactTypeEmail: { - required: []string{"email", "verification"}, - disallowed: []string{"phone", "walletAddress"}, + required: []string{emailHeader, verificationHeader}, + disallowed: []string{phoneHeader, walletAddressHeader}, }, data.RegistrationContactTypeEmailAndWalletAddress: { - required: []string{"email", "walletAddress"}, - disallowed: []string{"phone", "verification"}, + required: []string{emailHeader, walletAddressHeader}, + disallowed: []string{phoneHeader, verificationHeader}, }, data.RegistrationContactTypePhoneAndWalletAddress: { - required: []string{"phone", "walletAddress"}, - disallowed: []string{"email", "verification"}, + required: []string{phoneHeader, walletAddressHeader}, + disallowed: []string{emailHeader, verificationHeader}, }, } diff --git a/internal/serve/httphandler/disbursement_handler_test.go b/internal/serve/httphandler/disbursement_handler_test.go index ad1f87f19..c7eab8e64 100644 --- a/internal/serve/httphandler/disbursement_handler_test.go +++ b/internal/serve/httphandler/disbursement_handler_test.go @@ -11,7 +11,7 @@ import ( "mime/multipart" "net/http" "net/http/httptest" - "strings" + "net/url" "testing" "time" @@ -988,6 +988,16 @@ func Test_DisbursementHandler_PostDisbursementInstructions(t *testing.T) { expectedStatus: http.StatusOK, expectedMessage: "File uploaded successfully", }, + { + name: "valid input with BOM", + disbursementID: phoneDraftDisbursement.ID, + csvRecords: [][]string{ + {"\xef\xbb\xbf" + "phone", "id", "amount", "verification"}, + {"+380445555555", "123456789", "100.5", "1990-01-01"}, + }, + expectedStatus: http.StatusOK, + expectedMessage: "File uploaded successfully", + }, { name: ".bat file fails", disbursementID: phoneDraftDisbursement.ID, @@ -2114,13 +2124,15 @@ func createInstructionsMultipartRequest(t *testing.T, ctx context.Context, multi } func buildURLWithQueryParams(baseURL, endpoint string, queryParams map[string]string) string { - url := baseURL + endpoint - if len(queryParams) > 0 { - url += "?" - for k, v := range queryParams { - url += fmt.Sprintf("%s=%s&", k, v) - } - url = strings.TrimSuffix(url, "&") + u, err := url.Parse(baseURL + endpoint) + if err != nil { + panic(fmt.Sprintf("invalid URL: %v", err)) + } + + q := u.Query() + for k, v := range queryParams { + q.Set(k, v) } - return url + u.RawQuery = q.Encode() + return u.String() } diff --git a/internal/serve/httphandler/payments_handler_test.go b/internal/serve/httphandler/payments_handler_test.go index a99152dbc..730cf827a 100644 --- a/internal/serve/httphandler/payments_handler_test.go +++ b/internal/serve/httphandler/payments_handler_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/go-chi/chi/v5" + "github.com/google/uuid" "github.com/stellar/go/support/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -469,10 +470,10 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { // create receivers receiver1 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{}) - receiverWallet1 := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet.ID, data.DraftReceiversWalletStatus) + receiverWallet1 := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver1.ID, wallet.ID, data.RegisteredReceiversWalletStatus) receiver2 := data.CreateReceiverFixture(t, ctx, dbConnectionPool, &data.Receiver{}) - receiverWallet2 := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet.ID, data.DraftReceiversWalletStatus) + receiverWallet2 := data.CreateReceiverWalletFixture(t, ctx, dbConnectionPool, receiver2.ID, wallet.ID, data.RegisteredReceiversWalletStatus) // create disbursements disbursement1 := data.CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &data.Disbursement{ @@ -495,11 +496,12 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { require.NoError(t, err) // create payments - payment1 := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ + paymentDraft := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ Amount: "50", + ExternalPaymentID: uuid.NewString(), StellarTransactionID: stellarTransactionID, StellarOperationID: stellarOperationID, - Status: data.PendingPaymentStatus, + Status: data.DraftPaymentStatus, Disbursement: disbursement1, Asset: *asset, ReceiverWallet: receiverWallet1, @@ -512,11 +514,12 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { stellarOperationID, err = utils.RandomString(32) require.NoError(t, err) - payment2 := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ + paymentReady := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ Amount: "150", + ExternalPaymentID: uuid.NewString(), StellarTransactionID: stellarTransactionID, StellarOperationID: stellarOperationID, - Status: data.DraftPaymentStatus, + Status: data.ReadyPaymentStatus, Disbursement: disbursement1, Asset: *asset, ReceiverWallet: receiverWallet2, @@ -529,11 +532,12 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { stellarOperationID, err = utils.RandomString(32) require.NoError(t, err) - payment3 := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ + paymentPending := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ Amount: "200.50", + ExternalPaymentID: uuid.NewString(), StellarTransactionID: stellarTransactionID, StellarOperationID: stellarOperationID, - Status: data.DraftPaymentStatus, + Status: data.PendingPaymentStatus, Disbursement: disbursement2, Asset: *asset, ReceiverWallet: receiverWallet1, @@ -546,11 +550,12 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { stellarOperationID, err = utils.RandomString(32) require.NoError(t, err) - payment4 := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ + paymentPaused := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ Amount: "20", + ExternalPaymentID: uuid.NewString(), StellarTransactionID: stellarTransactionID, StellarOperationID: stellarOperationID, - Status: data.PendingPaymentStatus, + Status: data.PausedPaymentStatus, Disbursement: disbursement2, Asset: *asset, ReceiverWallet: receiverWallet2, @@ -558,24 +563,56 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { UpdatedAt: time.Date(2023, 4, 10, 23, 40, 20, 1431, time.UTC), }) - tests := []struct { + var paymentSuccess *data.Payment + var paymentFailed *data.Payment + var paymentCanceled *data.Payment + + for i, paymentStatus := range []data.PaymentStatus{data.SuccessPaymentStatus, data.FailedPaymentStatus, data.CanceledPaymentStatus} { + payment := data.CreatePaymentFixture(t, ctx, dbConnectionPool, models.Payment, &data.Payment{ + Amount: "50", + ExternalPaymentID: uuid.NewString(), + StellarTransactionID: stellarTransactionID, + StellarOperationID: stellarOperationID, + Status: paymentStatus, + Disbursement: disbursement1, + Asset: *asset, + ReceiverWallet: receiverWallet1, + CreatedAt: time.Date(2024, time.Month(i+1), 10, 23, 40, 20, 1431, time.UTC), + UpdatedAt: time.Date(2024, time.Month(i+1), 10, 23, 40, 20, 1431, time.UTC), + }) + + switch paymentStatus { + case data.SuccessPaymentStatus: + paymentSuccess = payment + case data.FailedPaymentStatus: + paymentFailed = payment + case data.CanceledPaymentStatus: + paymentCanceled = payment + default: + panic(fmt.Sprintf("invalid payment status: %s", paymentStatus)) + } + } + + type TestCase struct { name string queryParams map[string]string expectedStatusCode int expectedPagination httpresponse.PaginationInfo expectedPayments []data.Payment - }{ + } + + tests := []TestCase{ { - name: "fetch all payments without filters", + name: "fetch all payments without filters will use the default sorter (updated_at DESC)", queryParams: map[string]string{}, expectedStatusCode: http.StatusOK, expectedPagination: httpresponse.PaginationInfo{ Next: "", Prev: "", Pages: 1, - Total: 4, + Total: 7, }, - expectedPayments: []data.Payment{*payment4, *payment1, *payment3, *payment2}, + expectedPayments: []data.Payment{*paymentCanceled, *paymentFailed, *paymentSuccess, *paymentPaused, *paymentDraft, *paymentPending, *paymentReady}, // default sorter: (updated_at DESC) }, { name: "fetch first page of payments with limit 1 and sort by created_at", @@ -589,10 +626,10 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { expectedPagination: httpresponse.PaginationInfo{ Next: "/payments?direction=asc&page=2&page_limit=1&sort=created_at", Prev: "", - Pages: 4, - Total: 4, + Pages: 7, + Total: 7, }, - expectedPayments: []data.Payment{*payment1}, + expectedPayments: []data.Payment{*paymentDraft}, }, { name: "fetch second page of payments with limit 1 and sort by created_at", @@ -606,15 +643,15 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { expectedPagination: httpresponse.PaginationInfo{ Next: "/payments?direction=asc&page=3&page_limit=1&sort=created_at", Prev: "/payments?direction=asc&page=1&page_limit=1&sort=created_at", - Pages: 4, - Total: 4, + Pages: 7, + Total: 7, }, - expectedPayments: []data.Payment{*payment2}, + expectedPayments: []data.Payment{*paymentReady}, }, { name: "fetch last page of payments with limit 1 and sort by created_at", queryParams: map[string]string{ - "page": "4", + "page": "7", "page_limit": "1", "sort": "created_at", "direction": "asc", @@ -622,28 +659,14 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { expectedStatusCode: http.StatusOK, expectedPagination: httpresponse.PaginationInfo{ Next: "", - Prev: "/payments?direction=asc&page=3&page_limit=1&sort=created_at", - Pages: 4, - Total: 4, + Prev: "/payments?direction=asc&page=6&page_limit=1&sort=created_at", + Pages: 7, + Total: 7, }, - expectedPayments: []data.Payment{*payment4}, + expectedPayments: []data.Payment{*paymentCanceled}, }, { - name: "fetch payments with status draft", - queryParams: map[string]string{ - "status": "dRaFt", - }, - expectedStatusCode: http.StatusOK, - expectedPagination: httpresponse.PaginationInfo{ - Next: "", - Prev: "", - Pages: 1, - Total: 2, - }, - expectedPayments: []data.Payment{*payment3, *payment2}, - }, - { - name: "fetch payments for receiver1", + name: "fetch payments for receiver1 with default sorter (updated_at DESC)", queryParams: map[string]string{ "receiver_id": receiver1.ID, }, @@ -652,12 +675,12 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { Next: "", Prev: "", Pages: 1, - Total: 2, + Total: 5, }, - expectedPayments: []data.Payment{*payment1, *payment3}, + expectedPayments: []data.Payment{*paymentCanceled, *paymentFailed, *paymentSuccess, *paymentDraft, *paymentPending}, // default sorter: (updated_at DESC) }, { - name: "fetch payments for receiver2", + name: "fetch payments for receiver2 with default sorter (updated_at DESC)", queryParams: map[string]string{ "receiver_id": receiver2.ID, }, @@ -668,12 +691,12 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { Pages: 1, Total: 2, }, - expectedPayments: []data.Payment{*payment4, *payment2}, + expectedPayments: []data.Payment{*paymentPaused, *paymentReady}, // default sorter: (updated_at DESC) }, { name: "returns empty list when receiver_id is not found", queryParams: map[string]string{ - "receiver_id": "invalid_id", + "receiver_id": "non_existing_id", }, expectedStatusCode: http.StatusOK, expectedPagination: httpresponse.PaginationInfo{ @@ -696,7 +719,7 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { Pages: 1, Total: 1, }, - expectedPayments: []data.Payment{*payment1}, + expectedPayments: []data.Payment{*paymentDraft}, }, { name: "fetch payments after 2023-03-01", @@ -708,9 +731,9 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { Next: "", Prev: "", Pages: 1, - Total: 1, + Total: 4, }, - expectedPayments: []data.Payment{*payment4}, + expectedPayments: []data.Payment{*paymentCanceled, *paymentFailed, *paymentSuccess, *paymentPaused}, // default sorter: (updated_at DESC) }, { name: "fetch payment created at after 2023-01-01 and before 2023-03-01", @@ -725,8 +748,73 @@ func Test_PaymentHandler_GetPayments_Success(t *testing.T) { Pages: 1, Total: 2, }, - expectedPayments: []data.Payment{*payment3, *payment2}, + expectedPayments: []data.Payment{*paymentPending, *paymentReady}, // default sorter: (updated_at DESC) + }, + { + name: "query[p.id]", + queryParams: map[string]string{ + "q": paymentDraft.ID[:5], + }, + expectedStatusCode: http.StatusOK, + expectedPagination: httpresponse.PaginationInfo{ + Next: "", Prev: "", + Pages: 1, Total: 1, + }, + expectedPayments: []data.Payment{*paymentDraft}, + }, + { + name: "query[p.external_payment_id]", + queryParams: map[string]string{ + "q": paymentReady.ExternalPaymentID[:5], + }, + expectedStatusCode: http.StatusOK, + expectedPagination: httpresponse.PaginationInfo{ + Next: "", Prev: "", + Pages: 1, Total: 1, + }, + expectedPayments: []data.Payment{*paymentReady}, + }, + { + name: "query[rw.stellar_address]", + queryParams: map[string]string{ + "q": receiverWallet1.StellarAddress[:5], + }, + expectedStatusCode: http.StatusOK, + expectedPagination: httpresponse.PaginationInfo{ + Next: "", Prev: "", + Pages: 1, Total: 5, + }, + expectedPayments: []data.Payment{*paymentCanceled, *paymentFailed, *paymentSuccess, *paymentDraft, *paymentPending}, // default sorter: (updated_at DESC) }, + { + name: "query[d.name]", + queryParams: map[string]string{ + "q": disbursement2.Name[5:], + }, + expectedStatusCode: http.StatusOK, + expectedPagination: httpresponse.PaginationInfo{ + Next: "", Prev: "", + Pages: 1, Total: 2, + }, + expectedPayments: []data.Payment{*paymentPaused, *paymentPending}, // default sorter: (updated_at DESC) + }, + } + + for _, payment := range []data.Payment{*paymentDraft, *paymentPending, *paymentReady, *paymentPaused, *paymentSuccess, *paymentFailed, *paymentCanceled} { + tests = append(tests, TestCase{ + name: "fetch payments with status=" + string(payment.Status), + queryParams: map[string]string{ + "status": string(payment.Status), + }, + expectedStatusCode: http.StatusOK, + expectedPagination: httpresponse.PaginationInfo{ + Next: "", + Prev: "", + Pages: 1, + Total: 1, + }, + expectedPayments: []data.Payment{payment}, + }) } for _, tc := range tests { diff --git a/main.go b/main.go index c221859bb..66972f117 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 = "3.3.0" +const Version = "3.4.0" // GitCommit is populated at build time by // go build -ldflags "-X main.GitCommit=$GIT_COMMIT"