diff --git a/.testcoverage.yml b/.testcoverage.yml deleted file mode 100644 index 6c1ed39..0000000 --- a/.testcoverage.yml +++ /dev/null @@ -1,45 +0,0 @@ -# Path to coverprofile file (output of `go test -coverprofile` command) -profile: cover.out - -# (optional) -# When specified reported file paths will not contain local prefix in the output -local-prefix: "ulascansenturk/service" - -# Holds coverage thresholds percentages, values should be in range [0-100] -threshold: - # (optional; default 0) - # The minimum coverage that each file should have - file: 0 - - # (optional; default 0) - # The minimum coverage that each package should have - package: 80 - - # (optional; default 0) - # The minimum total coverage project should have - total: 80 - -# Holds regexp rules which will exclude matched files or packages from coverage statistics -exclude: - # Exclude files or packages matching their paths - paths: - - ^internal/accounts - - ^internal/admintasks - - ^internal/api/routes.go - - ^internal/api/server - - ^internal/basaccounts - - ^internal/cache - - ^internal/constants - - ^internal/events - - ^internal/providers/instafin - - ^internal/shared - - ^internal/temporalworkflows/admintasks/workflows - - ^internal/temporalworkflows/shared/utils - - ^internal/temporalworkflows/cards/workflows - - ^internal/transactions - - ^internal/ledgersync - -override: - # Temporary solution to bypass the coverage for the internal/api/v2 package - - threshold: 10 - path: ^internal/api/v2 diff --git a/Dockerfile b/Dockerfile index 934b558..e7ae3c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/golang:1.22.5-alpine3.20 AS basedockerf +FROM public.ecr.aws/docker/library/golang:1.22.5-alpine3.20 AS base ARG GITHUB_USER ARG GITHUB_PASSWORD diff --git a/db/migrations/20240814201241_create_transactions.up.sql b/db/migrations/20240814201241_create_transactions.up.sql index 3221f70..711e75f 100644 --- a/db/migrations/20240814201241_create_transactions.up.sql +++ b/db/migrations/20240814201241_create_transactions.up.sql @@ -1,3 +1,5 @@ +CREATE TYPE transaction_status AS ENUM ('PENDING', 'SUCCESS', 'FAILURE'); + CREATE TABLE transactions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID, @@ -6,7 +8,7 @@ CREATE TABLE transactions ( currency_code VARCHAR(10) NOT NULL, reference_id UUID NOT NULL, metadata JSONB, - status VARCHAR(20) NOT NULL, + status transaction_status NOT NULL, transaction_type VARCHAR(20) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP diff --git a/entrypoint.sh b/entrypoint.sh index 013e9b3..1887460 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -9,4 +9,4 @@ echo "Running database migrations..." # Start the main application echo "Starting the application..." -exec go run cmd/server/main.go +exec go run ./cmd/server diff --git a/go.mod b/go.mod index 94272e6..20a8a99 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module ulascansenturk/service go 1.22.5 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/getkin/kin-openapi v0.124.0 github.com/getsentry/sentry-go v0.27.0 github.com/go-chi/chi/v5 v5.1.0 @@ -18,8 +19,10 @@ require ( github.com/samber/do v1.6.0 github.com/samber/lo v1.46.0 github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go/modules/redis v0.32.0 go.temporal.io/sdk v1.27.0 golang.org/x/crypto v0.25.0 + gorm.io/datatypes v1.2.1 gorm.io/driver/postgres v1.5.7 gorm.io/gorm v1.25.10 gorm.io/plugin/dbresolver v1.5.1 @@ -28,18 +31,36 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.3.2 // indirect - github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.11.5 // indirect github.com/ajg/form v1.5.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/errdefs v0.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.0.3+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/swag v0.22.8 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -49,23 +70,47 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/invopop/yaml v0.2.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/robfig/cron v1.2.0 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/testcontainers/testcontainers-go v0.32.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect go.temporal.io/api v1.36.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/net v0.27.0 // indirect @@ -78,6 +123,6 @@ require ( google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/mysql v1.5.4 // indirect + gorm.io/driver/mysql v1.5.6 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/go.sum b/go.sum index dd7a857..5fec392 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,22 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= +github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -12,25 +24,47 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= +github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= @@ -45,6 +79,13 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= @@ -66,14 +107,19 @@ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA= github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +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/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -86,6 +132,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -112,8 +160,8 @@ github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= @@ -130,6 +178,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -140,6 +190,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -148,10 +202,30 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -165,6 +239,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= @@ -183,19 +259,40 @@ github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU= github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k= github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= +github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME= +github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E= +github.com/testcontainers/testcontainers-go/modules/redis v0.32.0 h1:HW5Qo9qfLi5iwfS7cbXwG6qe8ybXGePcgGPEmVlVDlo= +github.com/testcontainers/testcontainers-go/modules/redis v0.32.0/go.mod h1:5kltdxVKZG0aP1iegeqKz4K8HHyP0wbkW5o84qLyMjY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -203,7 +300,25 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.temporal.io/api v1.36.0 h1:WdntOw9m38lFvMdMXuOO+3BQ0R8HpVLgtk9+f+FwiDk= go.temporal.io/api v1.36.0/go.mod h1:0nWIrFRVPlcrkopXqxir/UWOtz/NZCo+EE9IX4UwVxw= go.temporal.io/sdk v1.27.0 h1:C5oOE/IRyLcZaFoB13kEHsjvSHEnGcwT6bNys0HFFHk= @@ -251,17 +366,26 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h 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= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.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.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= @@ -312,18 +436,26 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.1 h1:r+g0bk4LPCW2v4+Ls7aeNgGme7JYdNDQ2VtvlNUfBh0= +gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs= gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= -gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso= -gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= +gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/plugin/dbresolver v1.5.1 h1:s9Dj9f7r+1rE3nx/Ywzc85nXptUEaeOO0pt27xdopM8= gorm.io/plugin/dbresolver v1.5.1/go.mod h1:l4Cn87EHLEYuqUncpEeTC2tTJQkjngPSD+lo8hIvcT0= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= logur.dev/adapter/zerolog v0.6.0 h1:n2chk0KgpxTnfAJ+PVVa29vMbqThwUGs35YIZPRb+mE= diff --git a/internal/api/testutils/support/redis.go b/internal/api/testutils/support/redis.go new file mode 100644 index 0000000..2b99273 --- /dev/null +++ b/internal/api/testutils/support/redis.go @@ -0,0 +1,70 @@ +package support + +import ( + "context" + "strings" + "time" + + "github.com/redis/go-redis/v9" + "github.com/samber/lo" + "github.com/testcontainers/testcontainers-go" + redisContainer "github.com/testcontainers/testcontainers-go/modules/redis" +) + +const ( + urlSegmentsCount = 2 + redisImage = "public.ecr.aws/docker/library/redis:7" + attempts = 10 +) + +type Redis struct { + Client *redis.Client + Container *redisContainer.RedisContainer + Host string + Port string +} + +func NewRedis() *Redis { + return &Redis{} +} + +func (r *Redis) SetUp() { + ctx := context.Background() + + var redisCtn *redisContainer.RedisContainer + + _, _, attemptErr := lo.AttemptWithDelay(attempts, 1*time.Second, func(_ int, _ time.Duration) error { + ctn, err := redisContainer.RunContainer(ctx, testcontainers.WithImage(redisImage)) + if err != nil { + return err + } + + redisCtn = ctn + + return nil + }) + if attemptErr != nil { + panic(attemptErr) + } + + host := lo.Must(redisCtn.Host(context.Background())) + endpoint := lo.Must(redisCtn.Endpoint(context.Background(), "")) + client := redis.NewClient(&redis.Options{ + Network: "tcp", + Addr: endpoint, + }) + + parts := strings.Split(endpoint, ":") + if len(parts) < urlSegmentsCount { + panic(parts) + } + + r.Host = host + r.Port = parts[1] + r.Container = redisCtn + r.Client = client +} + +func (r *Redis) TearDown() { + lo.Must0(r.Container.Terminate(context.Background())) +} diff --git a/internal/temporalworkflows/activities/mutex_test.go b/internal/temporalworkflows/activities/mutex_test.go new file mode 100644 index 0000000..8862cf3 --- /dev/null +++ b/internal/temporalworkflows/activities/mutex_test.go @@ -0,0 +1,155 @@ +//go:build tests_unit + +package activities + +import ( + "context" + "testing" + "time" + "ulascansenturk/service/internal/api/testutils/support" + + "github.com/go-redsync/redsync/v4" + "github.com/go-redsync/redsync/v4/redis/goredis/v9" + "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "github.com/samber/lo" + "github.com/stretchr/testify/suite" + redisContainer "github.com/testcontainers/testcontainers-go/modules/redis" + "github.com/ulascansenturk/service/internal/api/testutils/support" +) + +const maxContainerCreationAttempts = 10 + +type testSuiteMutex struct { + suite.Suite + + mutex *Mutex + redisContainer *redisContainer.RedisContainer + redisClient *redis.Client + rd *support.Redis +} + +func (s *testSuiteMutex) SetupTest() { + s.createRedisContainer() + + endpoint := lo.Must(s.redisContainer.Endpoint(context.Background(), "")) + + s.redisClient = redis.NewClient(&redis.Options{ + Network: "tcp", + Addr: endpoint, + }) + + pool := goredis.NewPool(s.redisClient) + locker := redsync.New(pool) + + s.mutex = NewMutex(locker) +} + +func (s *testSuiteMutex) TearDownTest() { + s.rd.TearDown() +} + +func (s *testSuiteMutex) createRedisContainer() { + s.rd = support.NewRedis() + s.rd.SetUp() + + s.redisContainer = s.rd.Container +} + +func TestMutex(t *testing.T) { + t.Parallel() + + suite.Run(t, new(testSuiteMutex)) +} + +func (s *testSuiteMutex) TestMutex_AcquireLock() { + ctx := context.Background() + + s.Run("when the lock is acquired successfully", func() { + params := MutexParams{ + Key: uuid.New().String(), + OwnershipToken: "owner-1", + TTL: 1 * time.Minute, + } + + err := s.mutex.AcquireLock(context.Background(), params) + + s.NoError(err) + + val, getKeyErr := s.redisClient.Get(ctx, params.Key).Result() + + s.Require().NoError(getKeyErr) + + s.Equal(params.OwnershipToken, val) + + }) + + s.Run("when the lock cannot be acquired", func() { + params := MutexParams{ + Key: uuid.New().String(), + OwnershipToken: "owner-1", + TTL: 1 * time.Minute, + } + + err := s.mutex.AcquireLock(context.Background(), params) + + s.NoError(err) + + val, getKeyErr := s.redisClient.Get(ctx, params.Key).Result() + + s.Require().NoError(getKeyErr) + + s.Equal(params.OwnershipToken, val) + + secondErr := s.mutex.AcquireLock(context.Background(), params) + + s.EqualError(secondErr, "redsync: failed to acquire lock") + }) +} + +func (s *testSuiteMutex) TestMutex_ReleaseLock() { + ctx := context.Background() + + s.Run("when lock is released successfully", func() { + params := MutexParams{ + Key: uuid.New().String(), + OwnershipToken: "owner-1", + TTL: 1 * time.Minute, + } + + acquireLockErr := s.mutex.AcquireLock(ctx, params) + s.Require().NoError(acquireLockErr) + + err := s.mutex.ReleaseLock(ctx, params) + + s.NoError(err) + + val, getKeyErr := s.redisClient.Get(ctx, params.Key).Result() + + s.Equal("", val) + s.EqualError(getKeyErr, redis.Nil.Error()) + }) + + s.Run("when releasing returns an error during the initial lock", func() { + shortCtx, cancelShortCtx := context.WithCancel(ctx) + params := MutexParams{ + Key: uuid.New().String(), + OwnershipToken: "owner-1", + TTL: 1 * time.Minute, + } + + acquireLockErr := s.mutex.AcquireLock(ctx, params) + s.Require().NoError(acquireLockErr) + + cancelShortCtx() + + err := s.mutex.ReleaseLock(shortCtx, params) + + s.ErrorIs(err, redsync.ErrFailed) + + val, getKeyErr := s.redisClient.Get(ctx, params.Key).Result() + + s.Equal("owner-1", val) + s.NoError(getKeyErr) + }) +} diff --git a/internal/temporalworkflows/activities/transaction_operations.go b/internal/temporalworkflows/activities/transaction_operations.go index 9a25ac5..a9fd3ea 100644 --- a/internal/temporalworkflows/activities/transaction_operations.go +++ b/internal/temporalworkflows/activities/transaction_operations.go @@ -3,6 +3,8 @@ package activities import ( "context" "fmt" + "gorm.io/datatypes" + "time" "ulascansenturk/service/internal/accounts" "ulascansenturk/service/internal/constants" "ulascansenturk/service/internal/helpers" @@ -55,46 +57,47 @@ func (t *TransactionOperations) Transfer(ctx context.Context, params TransferPar } - pendingOutGoingTransaction, err := t.createPendingOutgoingTransaction(ctx, params, *validAccounts.SourceAccount) - if err != nil { - return nil, err + pendingOutGoingTransaction, pendingOutGoingTransactionErr := t.createPendingOutgoingTransaction(ctx, params, *validAccounts.SourceAccount) + if pendingOutGoingTransactionErr != nil { + return nil, pendingOutGoingTransactionErr } - pendingFeeTrx, err := t.createPendingFeeTransaction(ctx, params, *validAccounts.SourceAccount) - if err != nil { - return nil, err + pendingFeeTrx, pendingFeeTrxErr := t.createPendingFeeTransaction(ctx, params, *validAccounts.SourceAccount) + if pendingFeeTrxErr != nil { + return nil, pendingFeeTrxErr } - pendingIncomingTransaction, err := t.createPendingIncomingTransaction(ctx, params, *validAccounts.DestinationAccount) - if err != nil { - return nil, err + pendingIncomingTransaction, pendingIncomingTransactionErr := t.createPendingIncomingTransaction(ctx, params, *validAccounts.DestinationAccount) + if pendingIncomingTransactionErr != nil { + return nil, pendingIncomingTransactionErr } - if err := t.updateAccountBalances(ctx, *validAccounts.SourceAccount, *validAccounts.DestinationAccount, params); err != nil { - return nil, err + if updateAccountBalanceErr := t.updateAccountBalances(ctx, *validAccounts.SourceAccount, *validAccounts.DestinationAccount, params); updateAccountBalanceErr != nil { + return nil, updateAccountBalanceErr } - if err := t.finalizeTransactions(ctx, pendingOutGoingTransaction, pendingIncomingTransaction, pendingFeeTrx); err != nil { - return nil, err + updatedTransactions, finalizeTranscationErr := t.finalizeTransactions(ctx, pendingOutGoingTransaction, pendingIncomingTransaction, pendingFeeTrx) + if finalizeTranscationErr != nil { + return nil, finalizeTranscationErr } - return t.createTransferResult(params, pendingOutGoingTransaction, pendingIncomingTransaction, pendingFeeTrx), nil + return t.createTransferResult(params, &updatedTransactions.OutgoingTrx, &updatedTransactions.IncomingTrx, updatedTransactions.FeeTrx), nil } func (t *TransactionOperations) createPendingOutgoingTransaction(ctx context.Context, params TransferParams, sourceAccount accounts.Account) (*transactions.Transaction, error) { - pendingOutgoingTransactionParams := &transactions.DBTransaction{ + pendingOutgoingTransactionParams := &transactions.Transaction{ UserID: &sourceAccount.UserID, Amount: params.Amount, AccountID: sourceAccount.ID, CurrencyCode: constants.CurrencyCode(sourceAccount.Currency), ReferenceID: params.SourceTransactionReferenceID, - Metadata: &map[string]interface{}{ + Metadata: datatypes.JSONMap(map[string]interface{}{ "OperationType": "Transfer", "LinkedTransactionID": params.SourceTransactionReferenceID.String(), "LinkedAccountID": sourceAccount.ID.String(), "DestinationAccountID": params.DestinationAccountID.String(), - "timestamp": t.timeProvider.Now(), - }, + "timestamp": t.timeProvider.Now().Format(time.RFC3339), + }), Status: constants.TransactionStatusPENDING, TransactionType: constants.TransactionTypeOUTBOUND, } @@ -109,18 +112,18 @@ func (t *TransactionOperations) createPendingFeeTransaction(ctx context.Context, if params.FeeAmount == nil { return nil, nil } - pendingOutgoingFeeTransactionParams := &transactions.DBTransaction{ + pendingOutgoingFeeTransactionParams := &transactions.Transaction{ UserID: &sourceAccount.UserID, Amount: *params.FeeAmount, AccountID: sourceAccount.ID, CurrencyCode: constants.CurrencyCode(sourceAccount.Currency), ReferenceID: params.FeeTransactionReferenceID, - Metadata: &map[string]interface{}{ + Metadata: datatypes.JSONMap(map[string]interface{}{ "OperationType": "Fee Transfer", "LinkedTransactionID": params.FeeTransactionReferenceID.String(), "LinkedAccountID": params.SourceAccountID.String(), - "timestamp": t.timeProvider.Now(), - }, + "timestamp": t.timeProvider.Now().Format(time.RFC3339), + }), Status: constants.TransactionStatusPENDING, TransactionType: constants.TransactionTypeOUTGOINGFEE, } @@ -132,20 +135,21 @@ func (t *TransactionOperations) createPendingFeeTransaction(ctx context.Context, } func (t *TransactionOperations) createPendingIncomingTransaction(ctx context.Context, params TransferParams, destinationAccount accounts.Account) (*transactions.Transaction, error) { - pendingIncomingTransactionParams := &transactions.DBTransaction{ + pendingIncomingTransactionParams := &transactions.Transaction{ UserID: &destinationAccount.UserID, Amount: params.Amount, AccountID: destinationAccount.ID, CurrencyCode: constants.CurrencyCode(destinationAccount.Currency), - Metadata: &map[string]interface{}{ + Metadata: datatypes.JSONMap(map[string]interface{}{ "OperationType": "Transfer", "LinkedTransactionID": params.DestinationTransactionReferenceID.String(), "LinkedAccountID": params.DestinationAccountID.String(), "DestinationAccountID": params.DestinationAccountID.String(), "SourceAccountID": params.SourceAccountID, - "timestamp": t.timeProvider.Now(), - }, - ReferenceID: params.DestinationTransactionReferenceID, + "timestamp": t.timeProvider.Now().Format(time.RFC3339), + }), + ReferenceID: params.DestinationTransactionReferenceID, + Status: constants.TransactionStatusPENDING, TransactionType: constants.TransactionTypeINBOUND, } @@ -170,25 +174,33 @@ func (t *TransactionOperations) updateAccountBalances(ctx context.Context, sourc return nil } -func (t *TransactionOperations) finalizeTransactions(ctx context.Context, outgoing, incoming, fee *transactions.Transaction) error { - outgoing.Status = constants.TransactionStatusSUCCESS - if err := t.transactionService.UpdateTransactionStatus(ctx, outgoing.ID, constants.TransactionStatusSUCCESS); err != nil { - return err +func (t *TransactionOperations) finalizeTransactions(ctx context.Context, outgoing, incoming, fee *transactions.Transaction) (*UpdatedTransactions, error) { + updatedOutgoingTrx, updatedOutgoingTrxErr := t.transactionService.UpdateTransactionStatus(ctx, outgoing.ID, constants.TransactionStatusSUCCESS) + if updatedOutgoingTrxErr != nil { + return nil, updatedOutgoingTrxErr } - incoming.Status = constants.TransactionStatusSUCCESS - if err := t.transactionService.UpdateTransactionStatus(ctx, incoming.ID, constants.TransactionStatusSUCCESS); err != nil { - return err + updatedIncomingTrx, updatedIncomingTrxErr := t.transactionService.UpdateTransactionStatus(ctx, incoming.ID, constants.TransactionStatusSUCCESS) + if updatedIncomingTrxErr != nil { + return nil, updatedIncomingTrxErr } + var feeTrx *transactions.Transaction if fee != nil { fee.Status = constants.TransactionStatusSUCCESS - if err := t.transactionService.UpdateTransactionStatus(ctx, fee.ID, constants.TransactionStatusSUCCESS); err != nil { - return err + updatedFeeTrx, updatedFeeTrxErr := t.transactionService.UpdateTransactionStatus(ctx, fee.ID, constants.TransactionStatusSUCCESS) + if updatedFeeTrxErr != nil { + return nil, updatedFeeTrxErr } + feeTrx = updatedFeeTrx } - return nil + return &UpdatedTransactions{ + IncomingTrx: *updatedIncomingTrx, + OutgoingTrx: *updatedOutgoingTrx, + FeeTrx: feeTrx, + }, nil + } func (t *TransactionOperations) createTransferResult(params TransferParams, outgoing, incoming, fee *transactions.Transaction) *TransferResult { @@ -202,7 +214,7 @@ func (t *TransactionOperations) createTransferResult(params TransferParams, outg } } -func (t *TransactionOperations) findOrCreateTransaction(ctx context.Context, params *transactions.DBTransaction) (*transactions.Transaction, error) { +func (t *TransactionOperations) findOrCreateTransaction(ctx context.Context, params *transactions.Transaction) (*transactions.Transaction, error) { transaction, transactionErr := t.finderOrCreatorService.Call(ctx, params) if transactionErr != nil { return nil, transactionErr @@ -257,3 +269,9 @@ type ValidAccounts struct { SourceAccount *accounts.Account DestinationAccount *accounts.Account } + +type UpdatedTransactions struct { + IncomingTrx transactions.Transaction + OutgoingTrx transactions.Transaction + FeeTrx *transactions.Transaction +} diff --git a/internal/temporalworkflows/activities/transactions_operations_test.go b/internal/temporalworkflows/activities/transactions_operations_test.go index c804619..5cb5aa5 100644 --- a/internal/temporalworkflows/activities/transactions_operations_test.go +++ b/internal/temporalworkflows/activities/transactions_operations_test.go @@ -2,6 +2,10 @@ package activities import ( "context" + "gorm.io/datatypes" + "testing" + "time" + "github.com/DATA-DOG/go-sqlmock" "github.com/google/uuid" "github.com/stretchr/testify/mock" @@ -9,15 +13,12 @@ import ( "github.com/stretchr/testify/suite" "gorm.io/driver/postgres" "gorm.io/gorm" - "testing" - "time" "ulascansenturk/service/internal/accounts" accountMocks "ulascansenturk/service/internal/accounts/mocks" "ulascansenturk/service/internal/constants" + mockTime "ulascansenturk/service/internal/helpers/mocks" "ulascansenturk/service/internal/transactions" "ulascansenturk/service/internal/transactions/mocks" - - mockTime "ulascansenturk/service/internal/helpers/mocks" "ulascansenturk/service/internal/users" ) @@ -61,14 +62,12 @@ func TestTransactionsOperationsSuite(t *testing.T) { } func (s *transactionOperationsSuite) TestTransactionOperations() { - s.Run("Process Tranfer Activity", func() { + s.Run("Process Transfer Activity", func() { sourceAccID := uuid.New() - destinationAccID := uuid.New() - sourceAccountUser := users.User{ ID: uuid.New(), - Email: "ulas@gmail.com ", + Email: "ulas@gmail.com", FirstName: "ulas", IsActive: true, } @@ -78,14 +77,14 @@ func (s *transactionOperationsSuite) TestTransactionOperations() { UserID: sourceAccountUser.ID, Balance: 1000, Currency: "USD", - Status: "ACTIVE", + Status: constants.AccountStatusACTIVE, CreatedAt: time.Now(), UpdatedAt: time.Now(), } destinationAccountUser := users.User{ ID: uuid.New(), - Email: "meric@gmail.com ", + Email: "meric@gmail.com", FirstName: "meric", IsActive: true, } @@ -93,104 +92,95 @@ func (s *transactionOperationsSuite) TestTransactionOperations() { destinationAccount := accounts.Account{ ID: destinationAccID, UserID: destinationAccountUser.ID, - Balance: 1000, + Balance: 500, Currency: "USD", - Status: "ACTIVE", + Status: constants.AccountStatusACTIVE, CreatedAt: time.Now(), UpdatedAt: time.Now(), } - destinationTransactionReference := uuid.New() - - sourceTransactionReference := uuid.New() - transferParams := TransferParams{ - Amount: 400, - DestinationAccountID: destinationAccount.ID, - SourceTransactionReferenceID: sourceTransactionReference, - DestinationTransactionReferenceID: destinationTransactionReference, - SourceAccountID: sourceAccount.ID, + amount := 100 + feeAmount := 10 + + params := TransferParams{ + Amount: amount, + FeeAmount: &feeAmount, + DestinationAccountID: destinationAccID, + SourceTransactionReferenceID: uuid.New(), + DestinationTransactionReferenceID: uuid.New(), + FeeTransactionReferenceID: uuid.New(), + SourceAccountID: sourceAccID, } - s.accountsService.On("GetAccountByID", mock.Anything, sourceAccID).Return(&sourceAccount, nil).Once() + timestamp := time.Now() + s.timeProvider.On("Now").Return(timestamp) - s.accountsService.On("GetAccountByID", mock.Anything, destinationAccID).Return(&destinationAccount, nil).Once() + s.accountsService.On("GetAccountByID", mock.Anything, sourceAccID).Return(&sourceAccount, nil) + s.accountsService.On("GetAccountByID", mock.Anything, destinationAccID).Return(&destinationAccount, nil) - mockTime := time.Date(2024, 8, 17, 20, 13, 19, 662250000, time.UTC) - s.timeProvider.On("Now").Return(mockTime) + s.accountsService.On("UpdateBalance", mock.Anything, sourceAccID, amount, constants.BalanceOperationDECREASE.String()).Return(nil) + s.accountsService.On("UpdateBalance", mock.Anything, destinationAccID, amount, constants.BalanceOperationINCREASE.String()).Return(nil) - sourceTransactionMetadata := map[string]interface{}{ - "OperationType": "Transfer", - "LinkedTransactionID": transferParams.SourceTransactionReferenceID.String(), - "LinkedAccountID": sourceAccount.ID.String(), - "DestinationAccountID": transferParams.DestinationAccountID.String(), - "timestamp": mockTime, - } - - destinationTransactionMetadata := map[string]interface{}{ - "OperationType": "Transfer", - "LinkedTransactionID": transferParams.DestinationTransactionReferenceID.String(), - "LinkedAccountID": destinationAccount.ID.String(), - "DestinationAccountID": transferParams.DestinationAccountID.String(), - "SourceAccountID": transferParams.SourceAccountID, - "timestamp": mockTime, - } - - pendingOutGoingTransaction := &transactions.Transaction{ + sourceTransaction := &transactions.Transaction{ ID: uuid.New(), - UserID: &sourceAccount.UserID, - Amount: 400, - AccountID: sourceAccount.ID, - CurrencyCode: constants.CurrencyCode(sourceAccount.Currency), - ReferenceID: sourceTransactionReference, - Metadata: &sourceTransactionMetadata, + AccountID: sourceAccID, + Amount: amount, Status: constants.TransactionStatusPENDING, TransactionType: constants.TransactionTypeOUTBOUND, + Metadata: datatypes.JSONMap(map[string]interface{}{ + "OperationType": "Transfer", + "LinkedTransactionID": params.SourceTransactionReferenceID.String(), + "LinkedAccountID": sourceAccID.String(), + "DestinationAccountID": destinationAccID.String(), + "timestamp": timestamp.Format(time.RFC3339), + }), } - pendingIncomingTransaction := &transactions.Transaction{ + destinationTransaction := &transactions.Transaction{ ID: uuid.New(), - UserID: &destinationAccount.UserID, - Amount: 400, - AccountID: destinationAccount.ID, - CurrencyCode: constants.CurrencyCode(sourceAccount.Currency), - ReferenceID: destinationTransactionReference, - Metadata: &destinationTransactionMetadata, + AccountID: destinationAccID, + Amount: amount, Status: constants.TransactionStatusPENDING, TransactionType: constants.TransactionTypeINBOUND, + Metadata: datatypes.JSONMap(map[string]interface{}{ + "OperationType": "Transfer", + "LinkedTransactionID": params.DestinationTransactionReferenceID.String(), + "LinkedAccountID": destinationAccID.String(), + "SourceAccountID": sourceAccID.String(), + "timestamp": timestamp.Format(time.RFC3339), + }), } - s.finderOrCreatorService.On("Call", mock.Anything, &transactions.DBTransaction{ - UserID: &sourceAccount.UserID, - Amount: 400, - AccountID: sourceAccount.ID, - CurrencyCode: constants.CurrencyCode(sourceAccount.Currency), - ReferenceID: transferParams.SourceTransactionReferenceID, - Metadata: &sourceTransactionMetadata, - Status: constants.TransactionStatusPENDING, - TransactionType: constants.TransactionTypeOUTBOUND, - }).Return(pendingOutGoingTransaction, nil) - - s.finderOrCreatorService.On("Call", mock.Anything, &transactions.DBTransaction{ - UserID: &destinationAccount.UserID, - Amount: 400, - AccountID: destinationAccount.ID, - CurrencyCode: constants.CurrencyCode(sourceAccount.Currency), - ReferenceID: transferParams.DestinationTransactionReferenceID, - Metadata: &destinationTransactionMetadata, + feeTransaction := &transactions.Transaction{ + ID: uuid.New(), + AccountID: sourceAccID, + Amount: feeAmount, Status: constants.TransactionStatusPENDING, - TransactionType: constants.TransactionTypeINBOUND, - }).Return(pendingIncomingTransaction, nil) - - s.accountsService.On("UpdateBalance", mock.Anything, sourceAccID.ID, transferParams.Amount, constants.BalanceOperationDECREASE.String()).Return(nil).Once() - - s.accountsService.On("UpdateBalance", mock.Anything, destinationAccID.ID, transferParams.Amount, constants.BalanceOperationINCREASE.String()).Return(nil).Once() + TransactionType: constants.TransactionTypeOUTGOINGFEE, + Metadata: datatypes.JSONMap(map[string]interface{}{ + "OperationType": "Fee Transfer", + "LinkedTransactionID": params.FeeTransactionReferenceID.String(), + "LinkedAccountID": sourceAccID.String(), + "timestamp": timestamp.Format(time.RFC3339), + }), + } - s.transactionsService.On("UpdateTransactionStatus", mock.Anything, pendingOutGoingTransaction.ID, constants.TransactionStatusSUCCESS).Once() + s.finderOrCreatorService.On("Call", mock.Anything, mock.Anything).Return(sourceTransaction, nil).Once() + s.finderOrCreatorService.On("Call", mock.Anything, mock.Anything).Return(destinationTransaction, nil).Once() + s.finderOrCreatorService.On("Call", mock.Anything, mock.Anything).Return(feeTransaction, nil).Once() - s.transactionsService.On("UpdateTransactionStatus", mock.Anything, pendingIncomingTransaction.ID, constants.TransactionStatusSUCCESS).Once() + s.transactionsService.On("UpdateTransactionStatus", mock.Anything, sourceTransaction.ID, constants.TransactionStatusSUCCESS).Return(sourceTransaction, nil) + s.transactionsService.On("UpdateTransactionStatus", mock.Anything, destinationTransaction.ID, constants.TransactionStatusSUCCESS).Return(destinationTransaction, nil) + s.transactionsService.On("UpdateTransactionStatus", mock.Anything, feeTransaction.ID, constants.TransactionStatusSUCCESS).Return(feeTransaction, nil) - _, err := s.transactionOperations.Transfer(s.ctx, transferParams) + result, err := s.transactionOperations.Transfer(s.ctx, params) require.NoError(s.T(), err) + require.Equal(s.T(), params.SourceTransactionReferenceID, result.SourceTransactionReferenceID) + require.Equal(s.T(), params.DestinationTransactionReferenceID, result.DestinationTransactionReferenceID) + require.Equal(s.T(), params.FeeTransactionReferenceID, result.FeeTransactionReferenceID) + require.NotNil(s.T(), result.SourceTransaction) + require.NotNil(s.T(), result.DestinationTransaction) + require.NotNil(s.T(), result.FeeTransaction) }) } diff --git a/internal/temporalworkflows/transfer_test.go b/internal/temporalworkflows/transfer_test.go new file mode 100644 index 0000000..0b2510e --- /dev/null +++ b/internal/temporalworkflows/transfer_test.go @@ -0,0 +1,55 @@ +package temporalworkflows + +import ( + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "testing" + "ulascansenturk/service/internal/temporalworkflows/activities" + + temporalMocks "go.temporal.io/sdk/mocks" + "go.temporal.io/sdk/testsuite" +) + +type transfersTestSuite struct { + suite.Suite + testsuite.WorkflowTestSuite + + temporalService temporalMocks.Client + env *testsuite.TestWorkflowEnvironment +} + +func (s *transfersTestSuite) SetupSubTest() { + s.env = s.NewTestWorkflowEnvironment() + s.temporalService = temporalMocks.Client{} + + s.env.RegisterWorkflow(Transfer) +} + +func (s *transfersTestSuite) TearDownSubTest() { + s.env.AssertExpectations(s.T()) +} + +func TestInvalidateOutdatedVerifications(t *testing.T) { + t.Parallel() + + suite.Run(t, new(transfersTestSuite)) +} + +func (s *transfersTestSuite) TestTransfer() { + s.Run("Transfer", func() { + var transactionOperations activities.TransactionOperations + + activityResponse := &activities.TransferResult{} + + s.env.OnActivity( + transactionOperations.Transfer, + mock.Anything, + &TransferParams{}, + ).Return(activityResponse, nil) + + s.env.ExecuteWorkflow(Transfer) + + s.True(s.env.IsWorkflowCompleted()) + s.NoError(s.env.GetWorkflowError()) + }) +} diff --git a/internal/transactions/finder_or_creator.go b/internal/transactions/finder_or_creator.go index 01ef860..db93bff 100644 --- a/internal/transactions/finder_or_creator.go +++ b/internal/transactions/finder_or_creator.go @@ -9,7 +9,7 @@ import ( ) type FinderOrCreator interface { - Call(ctx context.Context, params *DBTransaction) (*Transaction, error) + Call(ctx context.Context, params *Transaction) (*Transaction, error) } type FinderOrCreatorService struct { @@ -26,7 +26,7 @@ func NewFinderOrCreatorService(transactionRepo Repository, accountRepo accounts. } } -func (s *FinderOrCreatorService) Call(ctx context.Context, params *DBTransaction) (*Transaction, error) { +func (s *FinderOrCreatorService) Call(ctx context.Context, params *Transaction) (*Transaction, error) { existingTransaction, err := s.transactionRepo.GetByReferenceID(ctx, params.ReferenceID) if err != nil { return nil, err @@ -67,8 +67,8 @@ func (s *FinderOrCreatorService) validAccounts(sourceAccount, destinationAccount return true } -func (s *FinderOrCreatorService) createTransaction(ctx context.Context, params *DBTransaction) (*Transaction, error) { - createdTransaction, createTransactionErr := s.transactionRepo.Create(ctx, FromDBTransaction(params)) +func (s *FinderOrCreatorService) createTransaction(ctx context.Context, params *Transaction) (*Transaction, error) { + createdTransaction, createTransactionErr := s.transactionRepo.Create(ctx, params) if createTransactionErr != nil { return nil, createTransactionErr } @@ -76,7 +76,7 @@ func (s *FinderOrCreatorService) createTransaction(ctx context.Context, params * return createdTransaction, nil } -func (s *FinderOrCreatorService) createFailedTransaction(ctx context.Context, params *DBTransaction, account *accounts.Account) (*Transaction, error) { +func (s *FinderOrCreatorService) createFailedTransaction(ctx context.Context, params *Transaction, account *accounts.Account) (*Transaction, error) { if params.Status != constants.TransactionStatusFAILURE { return nil, fmt.Errorf("only FAILURE transaction can be created for not INACTIVE accounts: %s", account.ID) } diff --git a/internal/transactions/mocks/mock_FinderOrCreator.go b/internal/transactions/mocks/mock_FinderOrCreator.go index 3fbfe08..1172848 100644 --- a/internal/transactions/mocks/mock_FinderOrCreator.go +++ b/internal/transactions/mocks/mock_FinderOrCreator.go @@ -15,7 +15,7 @@ type MockFinderOrCreator struct { } // Call provides a mock function with given fields: ctx, params -func (_m *MockFinderOrCreator) Call(ctx context.Context, params *transactions.DBTransaction) (*transactions.Transaction, error) { +func (_m *MockFinderOrCreator) Call(ctx context.Context, params *transactions.Transaction) (*transactions.Transaction, error) { ret := _m.Called(ctx, params) if len(ret) == 0 { @@ -24,10 +24,10 @@ func (_m *MockFinderOrCreator) Call(ctx context.Context, params *transactions.DB var r0 *transactions.Transaction var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *transactions.DBTransaction) (*transactions.Transaction, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *transactions.Transaction) (*transactions.Transaction, error)); ok { return rf(ctx, params) } - if rf, ok := ret.Get(0).(func(context.Context, *transactions.DBTransaction) *transactions.Transaction); ok { + if rf, ok := ret.Get(0).(func(context.Context, *transactions.Transaction) *transactions.Transaction); ok { r0 = rf(ctx, params) } else { if ret.Get(0) != nil { @@ -35,7 +35,7 @@ func (_m *MockFinderOrCreator) Call(ctx context.Context, params *transactions.DB } } - if rf, ok := ret.Get(1).(func(context.Context, *transactions.DBTransaction) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *transactions.Transaction) error); ok { r1 = rf(ctx, params) } else { r1 = ret.Error(1) diff --git a/internal/transactions/mocks/mock_Service.go b/internal/transactions/mocks/mock_Service.go index 08f8f31..a795e7b 100644 --- a/internal/transactions/mocks/mock_Service.go +++ b/internal/transactions/mocks/mock_Service.go @@ -239,21 +239,33 @@ func (_m *MockService) UpdateTransaction(ctx context.Context, transaction *trans } // UpdateTransactionStatus provides a mock function with given fields: ctx, id, status -func (_m *MockService) UpdateTransactionStatus(ctx context.Context, id uuid.UUID, status constants.TransactionStatus) error { +func (_m *MockService) UpdateTransactionStatus(ctx context.Context, id uuid.UUID, status constants.TransactionStatus) (*transactions.Transaction, error) { ret := _m.Called(ctx, id, status) if len(ret) == 0 { panic("no return value specified for UpdateTransactionStatus") } - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, constants.TransactionStatus) error); ok { + var r0 *transactions.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, constants.TransactionStatus) (*transactions.Transaction, error)); ok { + return rf(ctx, id, status) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, constants.TransactionStatus) *transactions.Transaction); ok { r0 = rf(ctx, id, status) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*transactions.Transaction) + } } - return r0 + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, constants.TransactionStatus) error); ok { + r1 = rf(ctx, id, status) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. diff --git a/internal/transactions/models.go b/internal/transactions/models.go index 169fd50..62ccc27 100644 --- a/internal/transactions/models.go +++ b/internal/transactions/models.go @@ -3,6 +3,8 @@ package transactions import ( "github.com/google/uuid" "time" + + "gorm.io/datatypes" "ulascansenturk/service/internal/constants" ) @@ -13,38 +15,9 @@ type Transaction struct { AccountID uuid.UUID `gorm:"type:uuid" json:"account_id"` CurrencyCode constants.CurrencyCode `gorm:"type:varchar(3)" json:"currency_code"` ReferenceID uuid.UUID `gorm:"type:uuid" json:"reference_id"` - Metadata *map[string]interface{} `gorm:"type:jsonb" json:"metadata"` + Metadata datatypes.JSONMap `gorm:"type:jsonb" json:"metadata"` Status constants.TransactionStatus `gorm:"type:varchar(50)" json:"status"` TransactionType constants.TransactionType `gorm:"type:varchar(50)" json:"transaction_type"` CreatedAt time.Time `gorm:"type:timestamptz;default:now()" json:"created_at,omitempty"` UpdatedAt time.Time `gorm:"type:timestamptz;default:now();autoUpdateTime()" json:"updated_at,omitempty"` } -type DBTransaction struct { - ID uuid.UUID `json:"id,omitempty"` - UserID *uuid.UUID `json:"user_id,omitempty"` - Amount int `json:"amount"` - AccountID uuid.UUID `json:"account_id"` - CurrencyCode constants.CurrencyCode `json:"currency_code"` - ReferenceID uuid.UUID `json:"reference_id"` - Metadata *map[string]interface{} `json:"metadata"` - Status constants.TransactionStatus `json:"status"` - TransactionType constants.TransactionType `json:"transaction_type"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` -} - -func FromDBTransaction(dbTransaction *DBTransaction) *Transaction { - return &Transaction{ - ID: dbTransaction.ID, - UserID: dbTransaction.UserID, - Amount: dbTransaction.Amount, - CurrencyCode: dbTransaction.CurrencyCode, - AccountID: dbTransaction.AccountID, - ReferenceID: dbTransaction.ReferenceID, - Metadata: dbTransaction.Metadata, - Status: dbTransaction.Status, - TransactionType: dbTransaction.TransactionType, - CreatedAt: dbTransaction.CreatedAt, - UpdatedAt: dbTransaction.UpdatedAt, - } -} diff --git a/internal/transactions/repository.go b/internal/transactions/repository.go index 537e6ee..61fafc1 100644 --- a/internal/transactions/repository.go +++ b/internal/transactions/repository.go @@ -7,6 +7,7 @@ import ( "gorm.io/gorm" "gorm.io/gorm/clause" "time" + "ulascansenturk/service/internal/constants" ) type Repository interface { @@ -17,12 +18,9 @@ type Repository interface { GetByToAccountID(ctx context.Context, toAccountID uuid.UUID) ([]*Transaction, error) GetByCreatedAt(ctx context.Context, createdAt time.Time) ([]*Transaction, error) Update(ctx context.Context, transaction *Transaction) error - - Transaction(ctx context.Context, fn func(*gorm.DB) error) error - + Transaction(ctx context.Context, fn func(*gorm.DB) (interface{}, error)) (interface{}, error) GetByIDForUpdate(ctx context.Context, transactionID uuid.UUID, tx *gorm.DB) (*Transaction, error) - - UpdateWithTx(ctx context.Context, transaction *Transaction, tx *gorm.DB) error + UpdateStatusWithTx(ctx context.Context, transaction Transaction, status constants.TransactionStatus, tx *gorm.DB) (*Transaction, error) Delete(ctx context.Context, id uuid.UUID) error DB() *gorm.DB } @@ -60,7 +58,7 @@ func (r *SQLRepository) GetByID(ctx context.Context, id uuid.UUID) (*Transaction func (r *SQLRepository) GetByReferenceID(ctx context.Context, referenceID uuid.UUID) (*Transaction, error) { var transaction Transaction - if err := r.db.WithContext(ctx).First(&transaction, "reference_id= ?", referenceID).Error; err != nil { + if err := r.db.WithContext(ctx).Table("transactions").First(&transaction, "reference_id= ?", referenceID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } @@ -107,24 +105,33 @@ func (r *SQLRepository) Delete(ctx context.Context, id uuid.UUID) error { return nil } -func (r *SQLRepository) UpdateWithTx(ctx context.Context, transaction *Transaction, tx *gorm.DB) error { +func (r *SQLRepository) UpdateStatusWithTx(ctx context.Context, transaction Transaction, status constants.TransactionStatus, tx *gorm.DB) (*Transaction, error) { if tx == nil { - return errors.New("transaction is required") + return nil, errors.New("transaction is required") } - if err := tx.WithContext(ctx).Model(&Transaction{}).Where("id = ?", transaction.ID).Updates(transaction).Error; err != nil { - return err + + // Update the status + if err := tx.WithContext(ctx).Model(&transaction).Update("status", status).Error; err != nil { + return nil, err } - return nil + + // Fetch the updated transaction + var updatedTransaction Transaction + if err := tx.WithContext(ctx).First(&updatedTransaction, transaction.ID).Error; err != nil { + return nil, err + } + + return &updatedTransaction, nil } -func (r *SQLRepository) Transaction(ctx context.Context, fn func(tx *gorm.DB) error) error { - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - if err := fn(tx); err != nil { - return err - } - return nil +func (r *SQLRepository) Transaction(ctx context.Context, fn func(tx *gorm.DB) (interface{}, error)) (interface{}, error) { + var result interface{} + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var err error + result, err = fn(tx) + return err }) + return result, err } - func (r *SQLRepository) GetByIDForUpdate(ctx context.Context, transactionID uuid.UUID, tx *gorm.DB) (*Transaction, error) { var transaction Transaction diff --git a/internal/transactions/service.go b/internal/transactions/service.go index ef341f5..410bfed 100644 --- a/internal/transactions/service.go +++ b/internal/transactions/service.go @@ -18,7 +18,7 @@ type Service interface { GetTransactionsByCreatedAt(ctx context.Context, createdAt time.Time) ([]*Transaction, error) UpdateTransaction(ctx context.Context, transaction *Transaction, tx *gorm.DB) error DeleteTransaction(ctx context.Context, id uuid.UUID) error - UpdateTransactionStatus(ctx context.Context, id uuid.UUID, status constants.TransactionStatus) error + UpdateTransactionStatus(ctx context.Context, id uuid.UUID, status constants.TransactionStatus) (*Transaction, error) BeginTransaction(ctx context.Context) (*gorm.DB, error) // New method } @@ -106,31 +106,36 @@ func (s *TransactionServiceImpl) BeginTransaction(ctx context.Context) (*gorm.DB } return tx, nil } -func (s *TransactionServiceImpl) UpdateTransactionStatus(ctx context.Context, id uuid.UUID, status constants.TransactionStatus) error { - // Start a new transaction - return s.repo.Transaction(ctx, func(tx *gorm.DB) error { - // Get the transaction by ID +func (s *TransactionServiceImpl) UpdateTransactionStatus(ctx context.Context, id uuid.UUID, status constants.TransactionStatus) (*Transaction, error) { + result, err := s.repo.Transaction(ctx, func(tx *gorm.DB) (interface{}, error) { transaction, err := s.repo.GetByIDForUpdate(ctx, id, tx) if err != nil { - return err + return nil, err } if transaction == nil { - return errors.New("transaction not found") + return nil, errors.New("transaction not found") } - // Validate the status (optional, based on your requirements) if status == "" { - return errors.New("status cannot be empty") + return nil, errors.New("status cannot be empty") } - // Update the transaction status - transaction.Status = status - - // Update the transaction in the repository - if err := s.repo.UpdateWithTx(ctx, transaction, tx); err != nil { - return err + updatedTrx, updateTrxErr := s.repo.UpdateStatusWithTx(ctx, *transaction, status, tx) + if updateTrxErr != nil { + return nil, updateTrxErr } - return nil + return updatedTrx, nil }) + + if err != nil { + return nil, err + } + + updatedTransaction, ok := result.(*Transaction) + if !ok { + return nil, errors.New("unexpected result type") + } + + return updatedTransaction, nil } diff --git a/internal/transactions/service_test.go b/internal/transactions/service_test.go new file mode 100644 index 0000000..b9a98e6 --- /dev/null +++ b/internal/transactions/service_test.go @@ -0,0 +1,177 @@ +package transactions_test + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/driver/postgres" + "gorm.io/gorm" + + "ulascansenturk/service/internal/constants" + "ulascansenturk/service/internal/transactions" +) + +func TestTransactionService_GetTransactionByID(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + gormDB, err := gorm.Open(postgres.New(postgres.Config{ + Conn: db, + }), &gorm.Config{}) + require.NoError(t, err) + + repo := transactions.NewSQLRepository(gormDB) + service := transactions.NewTransactionService(repo, validator.New()) + + ctx := context.Background() + transactionID := uuid.New() + + rows := sqlmock.NewRows([]string{"id", "amount", "status"}). + AddRow(transactionID, 100.0, constants.TransactionStatusPENDING) + + mock.ExpectQuery(`SELECT \* FROM "transactions" WHERE id = \$1`). + WithArgs(transactionID). + WillReturnRows(rows) + + transaction, err := service.GetTransactionByID(ctx, transactionID) + require.NoError(t, err) + assert.Equal(t, transactionID, transaction.ID) + assert.Equal(t, constants.TransactionStatusPENDING, transaction.Status) + assert.Equal(t, 100.0, transaction.Amount) + + // Ensure all expectations were met + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestTransactionService_GetTransactionByReferenceID(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + gormDB, err := gorm.Open(postgres.New(postgres.Config{ + Conn: db, + }), &gorm.Config{}) + require.NoError(t, err) + + repo := transactions.NewSQLRepository(gormDB) + service := transactions.NewTransactionService(repo, validator.New()) + + ctx := context.Background() + referenceID := uuid.New() + + rows := sqlmock.NewRows([]string{"id", "reference_id", "amount", "status"}). + AddRow(uuid.New(), referenceID, 200.0, constants.TransactionStatusSUCCESS) + + mock.ExpectQuery(`SELECT \* FROM "transactions" WHERE reference_id = \$1`). + WithArgs(referenceID). + WillReturnRows(rows) + + transaction, err := service.GetTransactionByReferenceID(ctx, referenceID) + require.NoError(t, err) + assert.Equal(t, referenceID, transaction.ReferenceID) + assert.Equal(t, constants.TransactionStatusSUCCESS, transaction.Status) + assert.Equal(t, 200.0, transaction.Amount) + + // Ensure all expectations were met + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestTransactionService_UpdateTransaction(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + gormDB, err := gorm.Open(postgres.New(postgres.Config{ + Conn: db, + }), &gorm.Config{}) + require.NoError(t, err) + + repo := transactions.NewSQLRepository(gormDB) + service := transactions.NewTransactionService(repo, validator.New()) + + ctx := context.Background() + transaction := &transactions.Transaction{ + ID: uuid.New(), + Amount: 300.0, + Status: constants.TransactionStatusPENDING, + } + + mock.ExpectExec(`UPDATE "transactions" SET "amount"=\$1,"status"=\$2 WHERE "id"=\$3`). + WithArgs(transaction.Amount, transaction.Status, transaction.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err = service.UpdateTransaction(ctx, transaction, nil) + require.NoError(t, err) + + // Ensure all expectations were met + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestTransactionService_UpdateTransactionStatus(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + gormDB, err := gorm.Open(postgres.New(postgres.Config{ + Conn: db, + }), &gorm.Config{}) + require.NoError(t, err) + + repo := transactions.NewSQLRepository(gormDB) + service := transactions.NewTransactionService(repo, validator.New()) + + ctx := context.Background() + transactionID := uuid.New() + newStatus := constants.TransactionStatusSUCCESS + + // Mock getting the transaction + rows := sqlmock.NewRows([]string{"id", "status"}). + AddRow(transactionID, constants.TransactionStatusPENDING) + + mock.ExpectQuery(`SELECT \* FROM "transactions" WHERE id = \$1 FOR UPDATE`). + WithArgs(transactionID). + WillReturnRows(rows) + + // Mock updating the transaction status + mock.ExpectExec(`UPDATE "transactions" SET "status" = \$1 WHERE "id" = \$2`). + WithArgs(newStatus, transactionID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + updatedTransaction, err := service.UpdateTransactionStatus(ctx, transactionID, newStatus) + require.NoError(t, err) + assert.Equal(t, newStatus, updatedTransaction.Status) + + // Ensure all expectations were met + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestTransactionService_BeginTransaction(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer db.Close() + + gormDB, err := gorm.Open(postgres.New(postgres.Config{ + Conn: db, + }), &gorm.Config{}) + require.NoError(t, err) + + repo := transactions.NewSQLRepository(gormDB) + service := transactions.NewTransactionService(repo, validator.New()) + + ctx := context.Background() + + mock.ExpectBegin() + + tx, err := service.BeginTransaction(ctx) + require.NoError(t, err) + assert.NotNil(t, tx) + + // Ensure all expectations were met + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/kube/postgres-deployment.yaml b/kube/postgres-deployment.yaml deleted file mode 100644 index 6f4a734..0000000 --- a/kube/postgres-deployment.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: postgres -spec: - replicas: 1 - selector: - matchLabels: - app: postgres - template: - metadata: - labels: - app: postgres - spec: - containers: - - name: postgres - image: postgres:13 - ports: - - containerPort: 5432 - env: - - name: POSTGRES_USER - value: "root" - - name: POSTGRES_PASSWORD - value: "service-password" - - name: POSTGRES_DB - value: "service" - diff --git a/kube/redis-deployment.yaml b/kube/redis-deployment.yaml deleted file mode 100644 index 0b50f59..0000000 --- a/kube/redis-deployment.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis -spec: - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - containers: - - name: redis - image: redis:alpine - ports: - - containerPort: 6379 - diff --git a/temporal.yaml b/temporal.yaml deleted file mode 100644 index 9d02083..0000000 --- a/temporal.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: temporal - namespace: myapp-namespace -spec: - replicas: 1 - selector: - matchLabels: - app: temporal - template: - metadata: - labels: - app: temporal - spec: - containers: - - name: temporal - image: temporalio/auto-setup:1.13.1 - ports: - - containerPort: 7233 - readinessProbe: - httpGet: - path: /health - port: 8233 - initialDelaySeconds: 5 - periodSeconds: 5 - ---- -apiVersion: v1 -kind: Service -metadata: - name: temporal - namespace: myapp-namespace -spec: - ports: - - port: 7233 - targetPort: 7233 - selector: - app: temporal -