Compare commits
161 Commits
1f67fba3e4
...
SEND066
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0172176978 | ||
| fdd8dd8845 | |||
|
|
3b23eada33 | ||
| b4b9507e7e | |||
|
|
6911757e1d | ||
| fba992f898 | |||
|
|
5a8392a6d0 | ||
| 6b82825494 | |||
|
|
54e9821886 | ||
| c74444ab0b | |||
| aba63ecd37 | |||
|
|
9ad2104d7d | ||
| e446486b77 | |||
|
|
65b04fba39 | ||
|
|
bbdbd9a197 | ||
|
|
049b23516a | ||
| 3862fa4e52 | |||
|
|
7ae4518926 | ||
|
|
e5b4de5d48 | ||
|
|
0e64ab5558 | ||
|
|
840a7f85c8 | ||
| 9c2b3bf8bd | |||
|
|
41720a6817 | ||
|
|
918dbe8bb5 | ||
|
|
6cc0340ba3 | ||
|
|
e77d1ab793 | ||
| d87e709f43 | |||
|
|
185f8f2ed6 | ||
| 2160b6bf4d | |||
|
|
54bbe41f7a | ||
|
|
f7f0612af9 | ||
|
|
0aceb2f441 | ||
|
|
281b3834d3 | ||
| 6633a1d807 | |||
|
|
88b279dd78 | ||
| 0f42a0e77f | |||
|
|
10bcdb4fe2 | ||
|
|
2b0ada1541 | ||
| ea5ec79a6e | |||
|
|
3295b9d9f0 | ||
| 031b8931ca | |||
|
|
4295456f63 | ||
| 2b1b4135f4 | |||
|
|
c60e7d2329 | ||
| be49254769 | |||
|
|
34e507b664 | ||
|
|
b67d199427 | ||
| b481de9ffc | |||
|
|
0c29e7686d | ||
| 5b26a70a15 | |||
|
|
b832c2a7c4 | ||
|
|
97b16542c2 | ||
|
|
39c04beb21 | ||
| 15393765b9 | |||
|
|
440b6a2553 | ||
| bc76cfe063 | |||
|
|
ed8f7c519c | ||
|
|
71d99338f2 | ||
| b499778bce | |||
|
|
4a554833c4 | ||
| b7ea11a62b | |||
|
|
026f698d9b | ||
| 0da6078468 | |||
|
|
3b65a2dc3a | ||
|
|
d6a3a0cc5b | ||
| a9b00b6871 | |||
| d64ad89072 | |||
|
|
4a5e26b03a | ||
|
|
d61eee99bc | ||
| 1e376da719 | |||
| a8b0c70b65 | |||
|
|
8981d296c8 | ||
|
|
7e5a98acd7 | ||
| 8577239dd6 | |||
|
|
5e59fea7e5 | ||
| 801f349aa8 | |||
|
|
d1e47841cc | ||
| 364731a8c7 | |||
|
|
519a2b1304 | ||
| d027f2deda | |||
|
|
ba5a3312b5 | ||
| f2c9685eb1 | |||
|
|
e80cb3eed1 | ||
| 5f647904d7 | |||
|
|
b6f05f52dc | ||
| 75555520f3 | |||
|
|
d666c4ce51 | ||
| 706a57e860 | |||
|
|
f7b0915303 | ||
|
|
c59538869b | ||
|
|
aff804ec58 | ||
| 2bab8371b8 | |||
|
|
af8ab8238e | ||
|
|
92a6191014 | ||
| 80b25a8608 | |||
| 17d954c689 | |||
|
|
349e8afdc5 | ||
|
|
8a1e44c038 | ||
|
|
3fcbbfb08a | ||
|
|
75f3678b90 | ||
| 4fa641f971 | |||
|
|
eb8b7b3402 | ||
| 3a8935f5f0 | |||
|
|
d92be5eedc | ||
| 94406373e6 | |||
| eda5bf19ad | |||
|
|
5629f5fcb2 | ||
| 95f7698661 | |||
|
|
4e70873a94 | ||
|
|
de07b9a792 | ||
| 9b794a3065 | |||
|
|
56bf49aa03 | ||
|
|
8377b6b2af | ||
| f06208348b | |||
|
|
b4c09cfb3b | ||
| 00812fa2bd | |||
|
|
ce5f90939f | ||
| 1b40b173eb | |||
| cd8e8071a9 | |||
|
|
f7a1027de7 | ||
| c5b3dfbd7a | |||
|
|
41cb826d26 | ||
|
|
51c72a87ae | ||
|
|
3f578353da | ||
| 7cac494509 | |||
|
|
d8f0febc5e | ||
| 34a8a5d057 | |||
|
|
83745bcd10 | ||
|
|
f9acb47ad7 | ||
| b2cc3fe980 | |||
|
|
d28e8615a9 | ||
| 3d1157a5d3 | |||
|
|
bae4cd6e35 | ||
| bd79eb016a | |||
|
|
b10ec79fe0 | ||
| 4b57550c36 | |||
|
|
0f0529c445 | ||
| 01c4108157 | |||
|
|
3c6cffdf33 | ||
| 82bab11a8f | |||
|
|
2f77d9d972 | ||
| 7559d4d09b | |||
| a1e739ba52 | |||
|
|
2be76aa519 | ||
|
|
6bb3ab5063 | ||
| 17e08ff26f | |||
|
|
e5ba048c73 | ||
| ddd5e36275 | |||
|
|
ce23de94ce | ||
| 1005201bb7 | |||
|
|
38077c1ed8 | ||
| d0368f5a00 | |||
|
|
86eab3bb70 | ||
| 709df51512 | |||
|
|
f914575a65 | ||
|
|
a6d560eb10 | ||
| 3cbe07a1ec | |||
|
|
598510f487 | ||
|
|
12c67361dd | ||
| 363d6474f2 | |||
|
|
004355f7d5 |
@@ -1,2 +1,18 @@
|
||||
ci/dev/mongo.key*
|
||||
|
||||
# VCS / editor files
|
||||
.git
|
||||
.vscode
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Local caches and temporary artifacts
|
||||
.cache
|
||||
.gocache
|
||||
**/.gocache
|
||||
**/tmp/
|
||||
**/tmp/**
|
||||
|
||||
# Frontend local build artifacts (rebuilt in Docker)
|
||||
frontend/**/.dart_tool
|
||||
frontend/**/build
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,7 +10,9 @@ generate_protos.sh
|
||||
update_dep.sh
|
||||
test.sh
|
||||
.vscode/
|
||||
|
||||
.gocache/
|
||||
.golangci-cache/
|
||||
.cache/
|
||||
.claude/
|
||||
|
||||
|
||||
@@ -4,8 +4,12 @@ matrix:
|
||||
BFF_DOCKERFILE: ci/prod/compose/bff.dockerfile
|
||||
BFF_MONGO_SECRET_PATH: sendico/db
|
||||
BFF_API_SECRET_PATH: sendico/api/endpoint
|
||||
BFF_VAULT_SECRET_PATH: sendico/edge/bff/vault
|
||||
BFF_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -46,6 +50,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh bff
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -74,8 +88,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/bff/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ matrix:
|
||||
DOCUMENTS_MONGO_SECRET_PATH: sendico/db
|
||||
DOCUMENTS_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -42,6 +45,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh billing_documents
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -70,8 +83,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/billing_documents/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ matrix:
|
||||
FEES_MONGO_SECRET_PATH: sendico/db
|
||||
FEES_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -42,6 +45,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh billing_fees
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -70,8 +83,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/billing_fees/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -6,6 +6,9 @@ matrix:
|
||||
CALLBACKS_VAULT_SECRET_PATH: sendico/edge/callbacks/vault
|
||||
CALLBACKS_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -43,6 +46,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh callbacks
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -71,8 +84,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/callbacks/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -4,6 +4,9 @@ matrix:
|
||||
DISCOVERY_DOCKERFILE: ci/prod/compose/discovery.dockerfile
|
||||
DISCOVERY_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -41,6 +44,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh discovery
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -69,8 +82,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/discovery/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -4,6 +4,9 @@ matrix:
|
||||
FRONTEND_DOCKERFILE: ci/prod/compose/frontend.dockerfile
|
||||
FRONTEND_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -22,7 +25,8 @@ steps:
|
||||
- name: version
|
||||
image: alpine:latest
|
||||
commands:
|
||||
- set -euo pipefail 2>/dev/null || set -eu
|
||||
- set -eu
|
||||
- if set -o | grep -q pipefail 2>/dev/null; then set -o pipefail; fi
|
||||
- apk add --no-cache git
|
||||
- GIT_REV="$(git rev-parse --short HEAD)"
|
||||
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
@@ -50,10 +54,21 @@ steps:
|
||||
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
|
||||
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
|
||||
|
||||
- name: frontend-tests
|
||||
image: ghcr.io/cirruslabs/flutter:stable
|
||||
depends_on: [ version ]
|
||||
commands:
|
||||
- set -eu
|
||||
- if set -o | grep -q pipefail 2>/dev/null; then set -o pipefail; fi
|
||||
- flutter --version
|
||||
- (cd frontend/pshared && flutter pub get && dart run build_runner build --delete-conflicting-outputs && flutter test)
|
||||
- (cd frontend/pweb && flutter pub get && dart run build_runner build --delete-conflicting-outputs && flutter test)
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ version, secrets ]
|
||||
depends_on: [ frontend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/frontend/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -8,6 +8,9 @@ matrix:
|
||||
FX_NEEDS_NATS: "true"
|
||||
FX_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -47,6 +50,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh fx_ingestor
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -75,8 +88,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/fx/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -8,6 +8,9 @@ matrix:
|
||||
FX_NEEDS_NATS: "true"
|
||||
FX_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -48,6 +51,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh fx_oracle
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -76,8 +89,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/fx/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -8,6 +8,9 @@ matrix:
|
||||
CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/chain/vault
|
||||
CHAIN_GATEWAY_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -46,6 +49,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh gateway_chain
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -74,8 +87,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "chain gateway image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/chain_gateway/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -7,6 +7,9 @@ matrix:
|
||||
MNTX_GATEWAY_NATS_SECRET_PATH: sendico/nats
|
||||
MNTX_GATEWAY_MONGO_SECRET_PATH: sendico/db
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -45,6 +48,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh gateway_mntx
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -73,8 +86,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/mntx/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ matrix:
|
||||
TGSETTLE_GATEWAY_MONGO_SECRET_PATH: sendico/db
|
||||
TGSETTLE_GATEWAY_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -43,6 +46,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh gateway_tgsettle
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -71,8 +84,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/tgsettle/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -8,6 +8,9 @@ matrix:
|
||||
TRON_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/tron/vault
|
||||
TRON_GATEWAY_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -46,6 +49,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh gateway_tron
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -74,8 +87,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/tron_gateway/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ matrix:
|
||||
LEDGER_MONGO_SECRET_PATH: sendico/db
|
||||
LEDGER_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -42,6 +45,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh ledger
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -70,8 +83,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/ledger/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -8,6 +8,9 @@ matrix:
|
||||
NOTIFICATION_TELEGRAM_SECRET_PATH: sendico/notification/telegram
|
||||
NOTIFICATION_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -45,6 +48,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh notification
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -73,8 +86,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/notification/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ matrix:
|
||||
PAYMENTS_METHODS_MONGO_SECRET_PATH: sendico/db
|
||||
PAYMENTS_METHODS_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -43,6 +46,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh payments_methods
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -71,8 +84,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/payments_methods/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ matrix:
|
||||
PAYMENTS_MONGO_SECRET_PATH: sendico/db
|
||||
PAYMENTS_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -43,6 +46,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh payments_orchestrator
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -71,8 +84,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/payments_orchestrator/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
@@ -5,6 +5,9 @@ matrix:
|
||||
PAYMENTS_QUOTATION_MONGO_SECRET_PATH: sendico/db
|
||||
PAYMENTS_QUOTATION_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -43,6 +46,16 @@ steps:
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- bash ci/scripts/proto/generate.sh
|
||||
|
||||
- name: backend-lint
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- export PATH="$(go env GOPATH)/bin:$PATH"
|
||||
- sh ci/scripts/common/run_backend_lint.sh payments_quotation
|
||||
|
||||
- name: backend-tests
|
||||
image: golang:alpine
|
||||
depends_on: [ proto ]
|
||||
@@ -71,8 +84,9 @@ steps:
|
||||
|
||||
- name: build-image
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
depends_on: [ backend-tests, secrets ]
|
||||
depends_on: [ backend-lint, backend-tests, secrets ]
|
||||
commands:
|
||||
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
|
||||
- sh ci/scripts/payments_quotation/build-image.sh
|
||||
|
||||
- name: deploy
|
||||
|
||||
155
Makefile
155
Makefile
@@ -1,10 +1,69 @@
|
||||
# Sendico Development Environment - Makefile
|
||||
# Docker Compose + Makefile build system
|
||||
|
||||
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend
|
||||
.PHONY: \
|
||||
help \
|
||||
init \
|
||||
build \
|
||||
build-infra \
|
||||
build-core \
|
||||
build-fx \
|
||||
build-payments \
|
||||
build-gateways \
|
||||
build-backend \
|
||||
build-frontend \
|
||||
up \
|
||||
down \
|
||||
restart \
|
||||
infra-up \
|
||||
services-up \
|
||||
backend-up \
|
||||
backend-down \
|
||||
backend-rebuild \
|
||||
status \
|
||||
list-services \
|
||||
logs \
|
||||
rebuild \
|
||||
clean \
|
||||
health \
|
||||
vault-init \
|
||||
proto \
|
||||
generate \
|
||||
generate-backend \
|
||||
generate-frontend \
|
||||
update \
|
||||
update-backend \
|
||||
update-frontend \
|
||||
test \
|
||||
test-backend \
|
||||
test-frontend \
|
||||
lint-backend
|
||||
|
||||
COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev
|
||||
SERVICE ?=
|
||||
BACKEND_GOCACHE ?= $(CURDIR)/.gocache
|
||||
BACKEND_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache
|
||||
BACKEND_SERVICES := \
|
||||
dev-discovery \
|
||||
dev-fx-oracle \
|
||||
dev-fx-ingestor \
|
||||
dev-billing-fees \
|
||||
dev-billing-documents \
|
||||
dev-ledger \
|
||||
dev-payments-orchestrator \
|
||||
dev-payments-quotation \
|
||||
dev-payments-methods \
|
||||
dev-chain-gateway-vault-agent \
|
||||
dev-chain-gateway \
|
||||
dev-tron-gateway-vault-agent \
|
||||
dev-tron-gateway \
|
||||
dev-aurora-gateway \
|
||||
dev-chsettle-gateway \
|
||||
dev-notification \
|
||||
dev-callbacks-vault-agent \
|
||||
dev-callbacks \
|
||||
dev-bff-vault-agent \
|
||||
dev-bff
|
||||
|
||||
# Colors
|
||||
GREEN := \033[0;32m
|
||||
@@ -24,34 +83,38 @@ help:
|
||||
@echo " make down Stop all services"
|
||||
@echo " make restart Restart all services"
|
||||
@echo " make status Show service status"
|
||||
@echo " make logs [SERVICE=x] View logs (all or specific service)"
|
||||
@echo " make rebuild SERVICE=x Rebuild specific service"
|
||||
@echo " make logs [SERVICE=dev-ledger] View logs (all or specific service)"
|
||||
@echo " make rebuild SERVICE=dev-ledger Rebuild specific service"
|
||||
@echo " make clean Remove all containers and volumes"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Selective Operations:$(NC)"
|
||||
@echo " make infra-up Start infrastructure only (mongo, nats, vault)"
|
||||
@echo " make services-up Start application services only"
|
||||
@echo " make backend-up Start backend services only (no infrastructure/frontend)"
|
||||
@echo " make backend-down Stop backend services only"
|
||||
@echo " make backend-rebuild Rebuild and restart backend services only"
|
||||
@echo " make list-services List all available services"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Build Groups:$(NC)"
|
||||
@echo " make build-core Build core services (discovery, ledger, fees, documents)"
|
||||
@echo " make build-fx Build FX services (oracle, ingestor)"
|
||||
@echo " make build-payments Build payment orchestrator"
|
||||
@echo " make build-gateways Build gateway services (chain, tron, mntx, tgsettle)"
|
||||
@echo " make build-api Build API services (notification, callbacks, bff)"
|
||||
@echo " make build-payments Build payment services (orchestrator, quotation, methods)"
|
||||
@echo " make build-gateways Build gateway services (chain, tron, aurora, chsettle)"
|
||||
@echo " make build-backend Build backend edge services (notification, callbacks, bff)"
|
||||
@echo " make build-frontend Build Flutter web frontend"
|
||||
@echo ""
|
||||
@echo "$(YELLOW)Development:$(NC)"
|
||||
@echo " make proto Generate protobuf code"
|
||||
@echo " make generate Generate all code (protobuf + Flutter)"
|
||||
@echo " make generate-api Generate protobuf code only"
|
||||
@echo " make generate-backend Generate protobuf code only"
|
||||
@echo " make generate-frontend Generate Flutter code only"
|
||||
@echo " make update Update all dependencies (Go + Flutter)"
|
||||
@echo " make update-api Update Go dependencies only"
|
||||
@echo " make update-backend Update Go dependencies only"
|
||||
@echo " make update-frontend Update Flutter dependencies only"
|
||||
@echo " make test Run all tests (API + frontend)"
|
||||
@echo " make test-api Run Go API tests only"
|
||||
@echo " make test Run all tests (backend + frontend)"
|
||||
@echo " make test-backend Run Go backend tests only"
|
||||
@echo " make test-frontend Run Flutter tests only"
|
||||
@echo " make lint-backend Run golangci-lint across all backend Go modules"
|
||||
@echo " make health Check service health"
|
||||
@echo ""
|
||||
@echo "Examples:"
|
||||
@@ -113,7 +176,7 @@ up:
|
||||
@echo " NATS UI: http://localhost:8222"
|
||||
@echo " Vault: http://localhost:8200 (run 'make vault-init' first)"
|
||||
@echo ""
|
||||
@echo "View logs: make logs [SERVICE=name]"
|
||||
@echo "View logs: make logs [SERVICE=dev-ledger]"
|
||||
@echo "Stop: make down"
|
||||
|
||||
# Stop all services
|
||||
@@ -137,7 +200,7 @@ endif
|
||||
# Rebuild specific service
|
||||
rebuild:
|
||||
ifndef SERVICE
|
||||
$(error SERVICE is required: make rebuild SERVICE=ledger)
|
||||
$(error SERVICE is required: make rebuild SERVICE=dev-ledger)
|
||||
endif
|
||||
@echo "$(GREEN)Rebuilding $(SERVICE)...$(NC)"
|
||||
@$(COMPOSE) build $(SERVICE)
|
||||
@@ -146,13 +209,13 @@ endif
|
||||
@echo "View logs: make logs SERVICE=$(SERVICE)"
|
||||
|
||||
# Generate protobuf code (alias)
|
||||
proto: generate-api
|
||||
proto: generate-backend
|
||||
|
||||
# Generate all code
|
||||
generate: generate-api generate-frontend
|
||||
generate: generate-backend generate-frontend
|
||||
|
||||
# Generate protobuf code
|
||||
generate-api:
|
||||
# Generate backend protobuf code
|
||||
generate-backend:
|
||||
@echo "$(GREEN)Generating protobuf code...$(NC)"
|
||||
@./ci/scripts/proto/generate.sh
|
||||
@echo "$(GREEN)✅ Protobuf generation complete$(NC)"
|
||||
@@ -222,13 +285,28 @@ services-up:
|
||||
dev-payments-methods \
|
||||
dev-chain-gateway \
|
||||
dev-tron-gateway \
|
||||
dev-mntx-gateway \
|
||||
dev-tgsettle-gateway \
|
||||
dev-aurora-gateway \
|
||||
dev-chsettle-gateway \
|
||||
dev-notification \
|
||||
dev-callbacks \
|
||||
dev-bff \
|
||||
dev-frontend
|
||||
|
||||
# Backend services only (no infrastructure, no frontend)
|
||||
backend-up:
|
||||
@echo "$(GREEN)Starting backend services only (no infra changes)...$(NC)"
|
||||
@$(COMPOSE) up -d --no-deps $(BACKEND_SERVICES)
|
||||
|
||||
backend-down:
|
||||
@echo "$(YELLOW)Stopping backend services only...$(NC)"
|
||||
@$(COMPOSE) stop $(BACKEND_SERVICES)
|
||||
|
||||
backend-rebuild:
|
||||
@echo "$(GREEN)Rebuilding backend services only (no infra changes)...$(NC)"
|
||||
@$(COMPOSE) build $(BACKEND_SERVICES)
|
||||
@$(COMPOSE) up -d --no-deps --force-recreate $(BACKEND_SERVICES)
|
||||
@echo "$(GREEN)✅ Backend services rebuilt$(NC)"
|
||||
|
||||
# Status check
|
||||
status:
|
||||
@$(COMPOSE) ps
|
||||
@@ -252,8 +330,8 @@ list-services:
|
||||
@echo " - dev-payments-methods :50066, :9416 (Payment Methods)"
|
||||
@echo " - dev-chain-gateway :50070, :9404 (EVM Blockchain Gateway)"
|
||||
@echo " - dev-tron-gateway :50071, :9408 (TRON Blockchain Gateway)"
|
||||
@echo " - dev-mntx-gateway :50075, :9405, :8084 (Card Payouts)"
|
||||
@echo " - dev-tgsettle-gateway :50080, :9406 (Telegram Settlements)"
|
||||
@echo " - dev-aurora-gateway :50075, :9405, :8084 (Card Payouts Simulator)"
|
||||
@echo " - dev-chsettle-gateway :50080, :9406 (Chimera Settlements Simulator)"
|
||||
@echo " - dev-notification :8081 (Notifications)"
|
||||
@echo " - dev-callbacks :9420 (Webhook Callbacks)"
|
||||
@echo " - dev-bff :8080 (Backend for Frontend)"
|
||||
@@ -283,10 +361,10 @@ build-payments:
|
||||
|
||||
build-gateways:
|
||||
@echo "$(GREEN)Building gateway services...$(NC)"
|
||||
@$(COMPOSE) build dev-chain-gateway dev-tron-gateway dev-mntx-gateway dev-tgsettle-gateway
|
||||
@$(COMPOSE) build dev-chain-gateway dev-tron-gateway dev-aurora-gateway dev-chsettle-gateway
|
||||
|
||||
build-api:
|
||||
@echo "$(GREEN)Building API services...$(NC)"
|
||||
build-backend:
|
||||
@echo "$(GREEN)Building backend edge services...$(NC)"
|
||||
@$(COMPOSE) build dev-notification dev-callbacks dev-bff
|
||||
|
||||
build-frontend:
|
||||
@@ -294,10 +372,10 @@ build-frontend:
|
||||
@$(COMPOSE) build dev-frontend
|
||||
|
||||
# Update all dependencies
|
||||
update: update-api update-frontend
|
||||
update: update-backend update-frontend
|
||||
|
||||
# Update Go API dependencies
|
||||
update-api:
|
||||
# Update Go backend dependencies
|
||||
update-backend:
|
||||
@echo "$(GREEN)Updating Go dependencies...$(NC)"
|
||||
@for dir in $$(find api -name go.mod -exec dirname {} \;); do \
|
||||
echo "Updating $$dir..."; \
|
||||
@@ -313,11 +391,11 @@ update-frontend:
|
||||
@echo "$(GREEN)✅ Flutter dependencies updated$(NC)"
|
||||
|
||||
# Run all tests
|
||||
test: test-api test-frontend
|
||||
test: test-backend test-frontend
|
||||
|
||||
# Run Go API tests
|
||||
test-api:
|
||||
@echo "$(GREEN)Running API tests...$(NC)"
|
||||
# Run Go backend tests
|
||||
test-backend:
|
||||
@echo "$(GREEN)Running backend tests...$(NC)"
|
||||
@failed=""; \
|
||||
for dir in $$(find api -name go.mod -exec dirname {} \;); do \
|
||||
echo "Testing $$dir..."; \
|
||||
@@ -327,7 +405,7 @@ test-api:
|
||||
echo "$(YELLOW)Failed:$$failed$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(GREEN)✅ All API tests passed$(NC)"
|
||||
@echo "$(GREEN)✅ All backend tests passed$(NC)"
|
||||
|
||||
# Run Flutter tests
|
||||
test-frontend:
|
||||
@@ -335,3 +413,18 @@ test-frontend:
|
||||
@cd frontend/pshared && flutter test
|
||||
@cd frontend/pweb && flutter test
|
||||
@echo "$(GREEN)✅ All frontend tests passed$(NC)"
|
||||
|
||||
# Run Go backend linting
|
||||
lint-backend:
|
||||
@echo "$(GREEN)Running backend linting...$(NC)"
|
||||
@mkdir -p "$(BACKEND_GOCACHE)" "$(BACKEND_GOLANGCI_LINT_CACHE)"
|
||||
@failed=""; \
|
||||
for dir in $$(find api -name go.mod -exec dirname {} \;); do \
|
||||
echo "Linting $$dir..."; \
|
||||
(cd "$$dir" && GOCACHE="$(BACKEND_GOCACHE)" GOLANGCI_LINT_CACHE="$(BACKEND_GOLANGCI_LINT_CACHE)" golangci-lint run --allow-serial-runners --allow-parallel-runners ./...) || failed="$$failed $$dir"; \
|
||||
done; \
|
||||
if [ -n "$$failed" ]; then \
|
||||
echo "$(YELLOW)Lint failed:$$failed$(NC)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(GREEN)✅ All backend lint checks passed$(NC)"
|
||||
|
||||
61
README.md
61
README.md
@@ -24,13 +24,25 @@ Financial services platform providing payment orchestration, ledger accounting,
|
||||
| FX Ingestor | `api/fx/ingestor/` | FX rate ingestion |
|
||||
| Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway |
|
||||
| Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway |
|
||||
| Gateway Aurora | `api/gateway/aurora/` | Card payouts simulator |
|
||||
| Gateway ChimeraSettle | `api/gateway/chsettle/` | Dummy settlement simulator (fast/slow/success/fail/stuck) |
|
||||
| Gateway MNTX | `api/gateway/mntx/` | Card payouts |
|
||||
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
|
||||
| Gateway TGSettle (legacy) | `api/gateway/tgsettle/` | Legacy Telegram settlement gateway (not used in dev compose) |
|
||||
| Notification | `api/notification/` | Notifications |
|
||||
| BFF | `api/edge/bff/` | Backend for frontend |
|
||||
| Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery |
|
||||
| Frontend | `frontend/pweb/` | Flutter web UI |
|
||||
|
||||
Gateway note: current dev compose workflows (`make services-up`, `make build-gateways`) use (`chain`, `tron`, `aurora`, `chsettle`). ChimeraSettle is the settlement simulator for test flows; it supports deterministic behavior routing via explicit scenario override and amount buckets. TGSettle remains in-repo as legacy code and is not started by default in dev compose.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker with Docker Compose plugin
|
||||
- GNU Make
|
||||
- Go toolchain
|
||||
- Dart SDK
|
||||
- Flutter SDK
|
||||
|
||||
## Development
|
||||
|
||||
Development uses Docker Compose via the Makefile. Run `make help` for all available commands.
|
||||
@@ -54,6 +66,8 @@ make status # Show service status
|
||||
make logs # View all logs
|
||||
make logs SERVICE=dev-ledger # View logs for a specific service
|
||||
make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service
|
||||
make list-services # List all services and ports
|
||||
make health # Check service health
|
||||
make clean # Remove all containers and volumes
|
||||
```
|
||||
|
||||
@@ -62,6 +76,10 @@ make clean # Remove all containers and volumes
|
||||
```bash
|
||||
make infra-up # Start infrastructure only (MongoDB, NATS, Vault)
|
||||
make services-up # Start application services only (assumes infra is running)
|
||||
make backend-up # Start backend services only (no infrastructure/frontend changes)
|
||||
make backend-down # Stop backend services only
|
||||
make backend-rebuild # Rebuild and restart backend services only
|
||||
make list-services # Show service names, ports, and descriptions
|
||||
```
|
||||
|
||||
### Build Groups
|
||||
@@ -69,9 +87,9 @@ make services-up # Start application services only (assumes infra is running)
|
||||
```bash
|
||||
make build-core # discovery, ledger, fees, documents
|
||||
make build-fx # oracle, ingestor
|
||||
make build-payments # orchestrator
|
||||
make build-gateways # chain, tron, mntx, tgsettle
|
||||
make build-api # notification, callbacks, bff
|
||||
make build-payments # orchestrator, quotation, methods
|
||||
make build-gateways # chain, tron, aurora, chsettle
|
||||
make build-backend # notification, callbacks, bff
|
||||
make build-frontend # Flutter web UI
|
||||
```
|
||||
|
||||
@@ -79,24 +97,43 @@ make build-frontend # Flutter web UI
|
||||
|
||||
```bash
|
||||
make generate # Generate all code (protobuf + Flutter)
|
||||
make generate-api # Generate protobuf code only
|
||||
make generate-backend # Generate protobuf code only
|
||||
make generate-frontend # Generate Flutter code only (build_runner)
|
||||
make proto # Alias for generate-api
|
||||
make proto # Alias for generate-backend
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
make test # Run all tests (API + frontend)
|
||||
make test-api # Run Go API tests only
|
||||
make test # Run all tests (backend + frontend)
|
||||
make test-backend # Run Go backend tests only
|
||||
make test-frontend # Run Flutter tests only
|
||||
```
|
||||
|
||||
### Backend CI Bypass Tags
|
||||
|
||||
Backend Woodpecker module pipelines now run both lint and tests before image build/deploy.
|
||||
If you intentionally need to bypass checks for a specific commit, include one of these tags in the commit message:
|
||||
|
||||
```text
|
||||
[skip-lint:<service>] # Skip lint for one backend service pipeline
|
||||
[skip-lint] # Skip lint for all backend service pipelines
|
||||
[skip-tests:<service>] # Skip tests for one backend service pipeline
|
||||
[skip-tests] # Skip tests for all backend service pipelines
|
||||
[skip-autotests:<service>] # Alias for skip-tests:<service>
|
||||
[skip-autotests] # Alias for skip-tests
|
||||
[skip-checks:<service>] # Skip both lint and tests for one backend service pipeline
|
||||
[skip-checks] # Skip both lint and tests for all backend service pipelines
|
||||
```
|
||||
|
||||
`<service>` must match the backend pipeline key used by CI:
|
||||
`bff`, `callbacks`, `billing_documents`, `billing_fees`, `discovery`, `fx_ingestor`, `fx_oracle`, `gateway_chain`, `gateway_mntx`, `gateway_tgsettle`, `gateway_tron`, `ledger`, `notification`, `payments_methods`, `payments_orchestrator`, `payments_quotation`.
|
||||
|
||||
### Update Dependencies
|
||||
|
||||
```bash
|
||||
make update # Update all Go and Flutter dependencies
|
||||
make update-api # Update Go dependencies only
|
||||
make update-backend # Update Go dependencies only
|
||||
make update-frontend # Update Flutter dependencies only
|
||||
```
|
||||
|
||||
@@ -126,11 +163,11 @@ Callbacks now authenticates to Vault through a sidecar Vault Agent (AppRole), sa
|
||||
Required Vault policy (minimal read-only for KV v2 mount `kv`):
|
||||
|
||||
```hcl
|
||||
path "kv/data/callbacks/*" {
|
||||
path "kv/data/sendico/callbacks/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "kv/metadata/callbacks/*" {
|
||||
path "kv/metadata/sendico/callbacks/*" {
|
||||
capabilities = ["read", "list"]
|
||||
}
|
||||
```
|
||||
@@ -158,5 +195,5 @@ vault kv put kv/sendico/edge/callbacks/vault \
|
||||
Store webhook signing secrets (example path consumed by `secret_ref`):
|
||||
|
||||
```bash
|
||||
vault kv put kv/callbacks/client-a/webhook secret="super-secret"
|
||||
vault kv put kv/sendico/callbacks/client-a/webhook secret="super-secret"
|
||||
```
|
||||
|
||||
7
SETUP.md
7
SETUP.md
@@ -137,10 +137,11 @@ make infra-up
|
||||
make services-up
|
||||
|
||||
# Or start specific service groups
|
||||
make build-core # discovery, ledger, billing-fees
|
||||
make build-core # discovery, ledger, billing-fees, billing-documents
|
||||
make build-fx # fx-oracle, fx-ingestor
|
||||
make build-payments # payments-orchestrator
|
||||
make build-gateways # chain, mntx, tgsettle
|
||||
make build-payments # payments-orchestrator, payments-quotation, payments-methods
|
||||
make build-gateways # chain, tron, aurora, chsettle
|
||||
make build-backend # notification, callbacks, bff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,196 +1,47 @@
|
||||
# See the dedicated "version" documentation section.
|
||||
version: "2"
|
||||
linters:
|
||||
# Default set of linters.
|
||||
# The value can be:
|
||||
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
|
||||
# - `all`: enables all linters by default.
|
||||
# - `none`: disables all linters by default.
|
||||
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
|
||||
# Default: standard
|
||||
default: all
|
||||
# Enable specific linter.
|
||||
default: none
|
||||
enable:
|
||||
- arangolint
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- cyclop
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- embeddedstructfieldcheck
|
||||
- err113
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exptostd
|
||||
- fatcontext
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funcorder
|
||||
- funlen
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gochecksumtype
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godoclint
|
||||
- godot
|
||||
- godox
|
||||
- goheader
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- govet
|
||||
- grouper
|
||||
- iface
|
||||
- importas
|
||||
- inamedparam
|
||||
- ineffassign
|
||||
- interfacebloat
|
||||
- intrange
|
||||
- iotamixing
|
||||
- ireturn
|
||||
- lll
|
||||
- loggercheck
|
||||
- maintidx
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- modernize
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnesserr
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- noctx
|
||||
- noinlineerr
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
- recvcheck
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- spancheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- tagalign
|
||||
- tagliatelle
|
||||
- testableexamples
|
||||
- testifylint
|
||||
- testpackage
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- unqueryvet
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- varnamelen
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- wsl_v5
|
||||
- zerologlint
|
||||
# Disable specific linters.
|
||||
disable:
|
||||
disable:
|
||||
- depguard
|
||||
- exhaustruct
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gomoddirectives
|
||||
- wsl
|
||||
- wrapcheck
|
||||
# All available settings of specific linters.
|
||||
# See the dedicated "linters.settings" documentation section.
|
||||
settings:
|
||||
wsl_v5:
|
||||
allow-first-in-block: true
|
||||
allow-whole-block: false
|
||||
branch-max-lines: 2
|
||||
|
||||
# Defines a set of rules to ignore issues.
|
||||
# It does not skip the analysis, and so does not ignore "typecheck" errors.
|
||||
exclusions:
|
||||
# Mode of the generated files analysis.
|
||||
#
|
||||
# - `strict`: sources are excluded by strictly following the Go generated file convention.
|
||||
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
|
||||
# This line must appear before the first non-comment, non-blank text in the file.
|
||||
# https://go.dev/s/generatedcode
|
||||
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
|
||||
# - `disable`: disable the generated files exclusion.
|
||||
#
|
||||
# Default: strict
|
||||
generated: lax
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
# Predefined exclusion rules.
|
||||
# Default: []
|
||||
presets:
|
||||
- comments
|
||||
- std-error-handling
|
||||
- common-false-positives
|
||||
- legacy
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source.
|
||||
rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- funlen
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
# Run some linter only for test files by excluding its issues for everything else.
|
||||
- path-except: _test\.go
|
||||
linters:
|
||||
- forbidigo
|
||||
# Exclude known linters from partially hard-vendored code,
|
||||
# which is impossible to exclude via `nolint` comments.
|
||||
# `/` will be replaced by the current OS file path separator to properly work on Windows.
|
||||
- path: internal/hmac/
|
||||
text: "weak cryptographic primitive"
|
||||
linters:
|
||||
- gosec
|
||||
# Exclude some `staticcheck` messages.
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA9003:"
|
||||
# Exclude `lll` issues for long lines with `go:generate`.
|
||||
- linters:
|
||||
- lll
|
||||
source: "^//go:generate "
|
||||
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
|
||||
# "/" will be replaced by the current OS file path separator to properly work on Windows.
|
||||
# Default: []
|
||||
paths: []
|
||||
# Which file paths to not exclude.
|
||||
# Default: []
|
||||
paths-except: []
|
||||
- cyclop
|
||||
- dupl
|
||||
- funlen
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- ireturn
|
||||
- lll
|
||||
- mnd
|
||||
- nestif
|
||||
- nlreturn
|
||||
- noinlineerr
|
||||
- paralleltest
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wsl_v5
|
||||
|
||||
BIN
api/billing/documents/assets/logo.png
Normal file
BIN
api/billing/documents/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -24,9 +24,7 @@ database:
|
||||
|
||||
documents:
|
||||
issuer:
|
||||
legal_name: "Sendico Ltd"
|
||||
legal_address: "12 Market Street, London, UK"
|
||||
logo_path: "/assets/logo.png"
|
||||
logo_path: "assets/logo.png"
|
||||
templates:
|
||||
acceptance_path: "templates/acceptance.tpl"
|
||||
protection:
|
||||
|
||||
@@ -24,11 +24,9 @@ database:
|
||||
|
||||
documents:
|
||||
issuer:
|
||||
legal_name: "Sendico Ltd"
|
||||
legal_address: "12 Market Street, London, UK"
|
||||
logo_path: "/assets/logo.png"
|
||||
logo_path: "/app/assets/logo.png"
|
||||
templates:
|
||||
acceptance_path: "templates/acceptance.tpl"
|
||||
acceptance_path: "/app/templates/acceptance.tpl"
|
||||
protection:
|
||||
owner_password: "sendico-documents"
|
||||
storage:
|
||||
|
||||
@@ -5,35 +5,35 @@ go 1.25.7
|
||||
replace github.com/tech/sendico/pkg => ../../pkg
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||
go.uber.org/zap v1.27.1
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/grpc v1.79.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
@@ -53,17 +53,17 @@ require (
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
|
||||
@@ -4,42 +4,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -158,8 +158,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
@@ -217,8 +217,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
@@ -233,16 +233,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -260,8 +260,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
116
api/billing/documents/internal/content/content.go
Normal file
116
api/billing/documents/internal/content/content.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package content
|
||||
|
||||
// Issuer details are intentionally centralized to avoid document text drift.
|
||||
const (
|
||||
IssuerLegalName = "SMX Operations Limited"
|
||||
IssuerLegalAddress = "Room 607, 12/F., Block C, Hong Kong Industrial Centre, 489-491 Castle Peak Road, Lai Chi Kok, Hong Kong"
|
||||
)
|
||||
|
||||
const (
|
||||
PDFTitleActOfAcceptance = "Act of Acceptance"
|
||||
DocumentIntegrityHashPrefix = "Document integrity hash: "
|
||||
)
|
||||
|
||||
// AcceptanceTemplateContent contains all static copy used by the acceptance act template.
|
||||
type AcceptanceTemplateContent struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
MetaDateLabel string
|
||||
MetaActNumberLabel string
|
||||
SectionParties string
|
||||
PartiesIntro string
|
||||
PartyExecutorLabel string
|
||||
PartyStatusLabel string
|
||||
PartyStatusValue string
|
||||
SectionBasis string
|
||||
BasisLine1 string
|
||||
BasisLine2 string
|
||||
SectionServicesRendered string
|
||||
ServicesRenderedLine1 string
|
||||
ServicesRenderedLine2 string
|
||||
SectionRemuneration string
|
||||
RemunerationHeaderDesc string
|
||||
RemunerationHeaderAmount string
|
||||
RemunerationServicesDesc string
|
||||
SectionConfirmation string
|
||||
ConfirmationLine1 string
|
||||
ConfirmationLine2 string
|
||||
ConfirmationPaymentLine1 string
|
||||
ConfirmationPaymentLine2 string
|
||||
SectionSignatures string
|
||||
SignatureCustomerLine string
|
||||
SignatureExecutorLine string
|
||||
}
|
||||
|
||||
var AcceptanceTemplate = AcceptanceTemplateContent{
|
||||
Title: "ACT OF ACCEPTANCE OF SERVICES",
|
||||
Subtitle: "under the Public Offer Agreement",
|
||||
MetaDateLabel: "Date",
|
||||
MetaActNumberLabel: "Act No",
|
||||
SectionParties: "PARTIES",
|
||||
PartiesIntro: "This Act is made between the following Parties.",
|
||||
PartyExecutorLabel: "Executor",
|
||||
PartyStatusLabel: "Status",
|
||||
PartyStatusValue: "Individual",
|
||||
SectionBasis: "BASIS",
|
||||
BasisLine1: "This Act is issued pursuant to the Public Offer Agreement",
|
||||
BasisLine2: "accepted by the Executor by joining the offer.",
|
||||
SectionServicesRendered: "SERVICES RENDERED",
|
||||
ServicesRenderedLine1: "The Executor has rendered services to the Customer",
|
||||
ServicesRenderedLine2: "in accordance with the terms of the Public Offer Agreement.",
|
||||
SectionRemuneration: "REMUNERATION",
|
||||
RemunerationHeaderDesc: "Description",
|
||||
RemunerationHeaderAmount: "Amount",
|
||||
RemunerationServicesDesc: "Services rendered under the Public Offer Agreement",
|
||||
SectionConfirmation: "CONFIRMATION",
|
||||
ConfirmationLine1: "The Customer confirms that the services were rendered properly",
|
||||
ConfirmationLine2: "and accepted without any claims.",
|
||||
ConfirmationPaymentLine1: "The remuneration for the services was paid to the Executor",
|
||||
ConfirmationPaymentLine2: "using the bank card details provided by the Executor.",
|
||||
SectionSignatures: "SIGNATURES",
|
||||
SignatureCustomerLine: "Customer ___________________________",
|
||||
SignatureExecutorLine: "Executor ___________________________",
|
||||
}
|
||||
|
||||
// OperationDocumentContent contains all static copy for operation documents.
|
||||
type OperationDocumentContent struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
MetaDocumentType string
|
||||
SectionOperation string
|
||||
SectionFailure string
|
||||
RowOrganization string
|
||||
RowGatewayService string
|
||||
RowOperationRef string
|
||||
RowPaymentRef string
|
||||
RowCode string
|
||||
RowState string
|
||||
RowLabel string
|
||||
RowStartedAtUTC string
|
||||
RowCompletedAtUTC string
|
||||
RowAmount string
|
||||
RowFailureCode string
|
||||
RowFailureReason string
|
||||
MissingValuePlaceholder string
|
||||
}
|
||||
|
||||
var OperationDocument = OperationDocumentContent{
|
||||
Title: "OPERATION BILLING DOCUMENT",
|
||||
Subtitle: "Gateway operation statement",
|
||||
MetaDocumentType: "Document Type: Operation",
|
||||
SectionOperation: "OPERATION DETAILS",
|
||||
SectionFailure: "FAILURE DETAILS",
|
||||
RowOrganization: "Organization",
|
||||
RowGatewayService: "Gateway Service",
|
||||
RowOperationRef: "Operation Ref",
|
||||
RowPaymentRef: "Payment Ref",
|
||||
RowCode: "Code",
|
||||
RowState: "State",
|
||||
RowLabel: "Label",
|
||||
RowStartedAtUTC: "Started At (UTC)",
|
||||
RowCompletedAtUTC: "Completed At (UTC)",
|
||||
RowAmount: "Amount",
|
||||
RowFailureCode: "Failure Code",
|
||||
RowFailureReason: "Failure Reason",
|
||||
MissingValuePlaceholder: "n/a",
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package docstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -36,8 +37,12 @@ func (s *LocalStore) Save(ctx context.Context, key string, data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(s.rootPath, filepath.Clean(key))
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
path, err := resolveStoragePath(s.rootPath, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path))
|
||||
|
||||
return err
|
||||
@@ -56,8 +61,12 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := filepath.Join(s.rootPath, filepath.Clean(key))
|
||||
path, err := resolveStoragePath(s.rootPath, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//nolint:gosec // path is constrained by resolveStoragePath to stay within configured root.
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path))
|
||||
@@ -69,3 +78,32 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
|
||||
}
|
||||
|
||||
var _ Store = (*LocalStore)(nil)
|
||||
|
||||
func resolveStoragePath(rootPath string, key string) (string, error) {
|
||||
cleanKey := filepath.Clean(strings.TrimSpace(key))
|
||||
if cleanKey == "" || cleanKey == "." {
|
||||
return "", merrors.InvalidArgument("docstore: key is required")
|
||||
}
|
||||
|
||||
if filepath.IsAbs(cleanKey) {
|
||||
return "", merrors.InvalidArgument("docstore: absolute keys are not allowed")
|
||||
}
|
||||
|
||||
rootAbs, err := filepath.Abs(rootPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve local store root: %w", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(rootAbs, cleanKey)
|
||||
pathAbs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve local store path: %w", err)
|
||||
}
|
||||
|
||||
prefix := rootAbs + string(filepath.Separator)
|
||||
if pathAbs != rootAbs && !strings.HasPrefix(pathAbs, prefix) {
|
||||
return "", merrors.InvalidArgument("docstore: key escapes root path")
|
||||
}
|
||||
|
||||
return pathAbs, nil
|
||||
}
|
||||
|
||||
@@ -124,7 +124,11 @@ func (s *S3Store) Load(ctx context.Context, key string) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer obj.Body.Close()
|
||||
defer func() {
|
||||
if closeErr := obj.Body.Close(); closeErr != nil {
|
||||
s.logger.Warn("Failed to close document body", zap.Error(closeErr), zap.String("key", key))
|
||||
}
|
||||
}()
|
||||
|
||||
return io.ReadAll(obj.Body)
|
||||
}
|
||||
|
||||
@@ -3,18 +3,24 @@ package documents
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/billing/documents/internal/content"
|
||||
"github.com/tech/sendico/billing/documents/internal/docstore"
|
||||
"github.com/tech/sendico/billing/documents/renderer"
|
||||
)
|
||||
|
||||
// Config holds document service settings loaded from YAML.
|
||||
type Config struct {
|
||||
Issuer renderer.Issuer `yaml:"issuer"`
|
||||
Issuer IssuerConfig `yaml:"issuer"`
|
||||
Templates TemplateConfig `yaml:"templates"`
|
||||
Protection ProtectionConfig `yaml:"protection"`
|
||||
Storage docstore.Config `yaml:"storage"`
|
||||
}
|
||||
|
||||
// IssuerConfig defines issuer settings that are environment-specific.
|
||||
type IssuerConfig struct {
|
||||
LogoPath string `yaml:"logo_path"`
|
||||
}
|
||||
|
||||
// TemplateConfig defines document template locations.
|
||||
type TemplateConfig struct {
|
||||
AcceptancePath string `yaml:"acceptance_path"`
|
||||
@@ -25,6 +31,14 @@ type ProtectionConfig struct {
|
||||
OwnerPassword string `yaml:"owner_password"`
|
||||
}
|
||||
|
||||
func (c Config) IssuerDetails() renderer.Issuer {
|
||||
return renderer.Issuer{
|
||||
LegalName: content.IssuerLegalName,
|
||||
LegalAddress: content.IssuerLegalAddress,
|
||||
LogoPath: c.Issuer.LogoPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) AcceptanceTemplatePath() string {
|
||||
if strings.TrimSpace(c.Templates.AcceptancePath) == "" {
|
||||
return "templates/acceptance.tpl"
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
@@ -17,7 +16,6 @@ var (
|
||||
|
||||
requestsTotal *prometheus.CounterVec
|
||||
requestLatency *prometheus.HistogramVec
|
||||
batchSize prometheus.Histogram
|
||||
documentBytes *prometheus.HistogramVec
|
||||
)
|
||||
|
||||
@@ -44,16 +42,6 @@ func initMetrics() {
|
||||
[]string{"call", "status", "doc_type"},
|
||||
)
|
||||
|
||||
batchSize = promauto.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "billing",
|
||||
Subsystem: "documents",
|
||||
Name: "batch_size",
|
||||
Help: "Number of payment references in batch resolution requests.",
|
||||
Buckets: []float64{0, 1, 2, 5, 10, 20, 50, 100, 250, 500},
|
||||
},
|
||||
)
|
||||
|
||||
documentBytes = promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "billing",
|
||||
@@ -67,18 +55,14 @@ func initMetrics() {
|
||||
})
|
||||
}
|
||||
|
||||
func observeRequest(call string, docType documentsv1.DocumentType, statusLabel string, took time.Duration) {
|
||||
typeLabel := docTypeLabel(docType)
|
||||
requestsTotal.WithLabelValues(call, statusLabel, typeLabel).Inc()
|
||||
requestLatency.WithLabelValues(call, statusLabel, typeLabel).Observe(took.Seconds())
|
||||
func observeRequest(call string, documentKind, statusLabel string, took time.Duration) {
|
||||
kind := docKindLabel(documentKind)
|
||||
requestsTotal.WithLabelValues(call, statusLabel, kind).Inc()
|
||||
requestLatency.WithLabelValues(call, statusLabel, kind).Observe(took.Seconds())
|
||||
}
|
||||
|
||||
func observeBatchSize(size int) {
|
||||
batchSize.Observe(float64(size))
|
||||
}
|
||||
|
||||
func observeDocumentBytes(docType documentsv1.DocumentType, size int) {
|
||||
documentBytes.WithLabelValues(docTypeLabel(docType)).Observe(float64(size))
|
||||
func observeDocumentBytes(documentKind string, size int) {
|
||||
documentBytes.WithLabelValues(docKindLabel(documentKind)).Observe(float64(size))
|
||||
}
|
||||
|
||||
func statusFromError(err error) string {
|
||||
@@ -100,10 +84,10 @@ func statusFromError(err error) string {
|
||||
return strings.ToLower(code.String())
|
||||
}
|
||||
|
||||
func docTypeLabel(docType documentsv1.DocumentType) string {
|
||||
label := docType.String()
|
||||
func docKindLabel(documentKind string) string {
|
||||
label := strings.TrimSpace(documentKind)
|
||||
if label == "" {
|
||||
return "DOCUMENT_TYPE_UNSPECIFIED"
|
||||
return "operation"
|
||||
}
|
||||
|
||||
return label
|
||||
|
||||
@@ -4,13 +4,12 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/billing/documents/internal/appversion"
|
||||
"github.com/tech/sendico/billing/documents/internal/content"
|
||||
"github.com/tech/sendico/billing/documents/internal/docstore"
|
||||
"github.com/tech/sendico/billing/documents/renderer"
|
||||
"github.com/tech/sendico/billing/documents/storage"
|
||||
@@ -146,131 +145,30 @@ func (s *Service) Shutdown() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) {
|
||||
func (s *Service) GetOperationDocument(_ context.Context, req *documentsv1.GetOperationDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
|
||||
start := time.Now()
|
||||
|
||||
var paymentRefs []string
|
||||
if req != nil {
|
||||
paymentRefs = req.GetPaymentRefs()
|
||||
}
|
||||
|
||||
logger := s.logger.With(zap.Int("payment_refs", len(paymentRefs)))
|
||||
|
||||
defer func() {
|
||||
statusLabel := statusFromError(err)
|
||||
observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start))
|
||||
observeBatchSize(len(paymentRefs))
|
||||
|
||||
itemsCount := 0
|
||||
if resp != nil {
|
||||
itemsCount = len(resp.GetItems())
|
||||
}
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.String("status", statusLabel),
|
||||
zap.Duration("duration", time.Since(start)),
|
||||
zap.Int("items", itemsCount),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("BatchResolveDocuments finished", fields...)
|
||||
}()
|
||||
|
||||
if len(paymentRefs) == 0 {
|
||||
resp = &documentsv1.BatchResolveDocumentsResponse{}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if s.storage == nil {
|
||||
err = status.Error(codes.Unavailable, errStorageUnavailable.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs := make([]string, 0, len(paymentRefs))
|
||||
for _, ref := range paymentRefs {
|
||||
clean := strings.TrimSpace(ref)
|
||||
|
||||
if clean == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
refs = append(refs, clean)
|
||||
}
|
||||
|
||||
if len(refs) == 0 {
|
||||
resp = &documentsv1.BatchResolveDocumentsResponse{}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
records, err := s.storage.Documents().ListByPaymentRefs(ctx, refs)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
recordByRef := map[string]*model.DocumentRecord{}
|
||||
|
||||
for _, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
recordByRef[record.PaymentRef] = record
|
||||
}
|
||||
|
||||
items := make([]*documentsv1.DocumentMeta, 0, len(refs))
|
||||
for _, ref := range refs {
|
||||
meta := &documentsv1.DocumentMeta{PaymentRef: ref}
|
||||
if record := recordByRef[ref]; record != nil {
|
||||
record.Normalize()
|
||||
|
||||
available := []model.DocumentType{model.DocumentTypeAct}
|
||||
|
||||
ready := make([]model.DocumentType, 0, 1)
|
||||
if path, ok := record.StoragePaths[model.DocumentTypeAct]; ok && path != "" {
|
||||
ready = append(ready, model.DocumentTypeAct)
|
||||
}
|
||||
|
||||
meta.AvailableTypes = toProtoTypes(available)
|
||||
meta.ReadyTypes = toProtoTypes(ready)
|
||||
}
|
||||
|
||||
items = append(items, meta)
|
||||
}
|
||||
|
||||
resp = &documentsv1.BatchResolveDocumentsResponse{Items: items}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
|
||||
start := time.Now()
|
||||
docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
|
||||
paymentRef := ""
|
||||
organizationRef := ""
|
||||
gatewayService := ""
|
||||
operationRef := ""
|
||||
|
||||
if req != nil {
|
||||
docType = req.GetType()
|
||||
paymentRef = strings.TrimSpace(req.GetPaymentRef())
|
||||
organizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||
gatewayService = strings.TrimSpace(req.GetGatewayService())
|
||||
operationRef = strings.TrimSpace(req.GetOperationRef())
|
||||
}
|
||||
|
||||
logger := s.logger.With(
|
||||
zap.String("payment_ref", paymentRef),
|
||||
zap.String("document_type", docTypeLabel(docType)),
|
||||
zap.String("organization_ref", organizationRef),
|
||||
zap.String("gateway_service", gatewayService),
|
||||
zap.String("operation_ref", operationRef),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
statusLabel := statusFromError(err)
|
||||
observeRequest("get_document", docType, statusLabel, time.Since(start))
|
||||
observeRequest("get_operation_document", "operation", statusLabel, time.Since(start))
|
||||
|
||||
if resp != nil {
|
||||
observeDocumentBytes(docType, len(resp.GetContent()))
|
||||
observeDocumentBytes("operation", len(resp.GetContent()))
|
||||
}
|
||||
|
||||
contentBytes := 0
|
||||
@@ -285,100 +183,49 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Warn("GetDocument failed", append(fields, zap.Error(err))...)
|
||||
logger.Warn("GetOperationDocument failed", append(fields, zap.Error(err))...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("GetDocument finished", fields...)
|
||||
logger.Info("GetOperationDocument finished", fields...)
|
||||
}()
|
||||
|
||||
if paymentRef == "" {
|
||||
err = status.Error(codes.InvalidArgument, "payment_ref is required")
|
||||
if req == nil {
|
||||
err = status.Error(codes.InvalidArgument, "request is required")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if docType == documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED {
|
||||
err = status.Error(codes.InvalidArgument, "document type is required")
|
||||
if organizationRef == "" {
|
||||
err = status.Error(codes.InvalidArgument, "organization_ref is required")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.storage == nil {
|
||||
err = status.Error(codes.Unavailable, errStorageUnavailable.Error())
|
||||
if gatewayService == "" {
|
||||
err = status.Error(codes.InvalidArgument, "gateway_service is required")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.docStore == nil {
|
||||
err = status.Error(codes.Unavailable, errDocStoreUnavailable.Error())
|
||||
if operationRef == "" {
|
||||
err = status.Error(codes.InvalidArgument, "operation_ref is required")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.template == nil {
|
||||
err = status.Error(codes.FailedPrecondition, errTemplateUnavailable.Error())
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record, err := s.storage.Documents().GetByPaymentRef(ctx, paymentRef)
|
||||
if err != nil {
|
||||
if errors.Is(err, storage.ErrDocumentNotFound) {
|
||||
return nil, status.Error(codes.NotFound, "document record not found")
|
||||
}
|
||||
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
record.Normalize()
|
||||
|
||||
targetType := model.DocumentTypeFromProto(docType)
|
||||
|
||||
if docType != documentsv1.DocumentType_DOCUMENT_TYPE_ACT {
|
||||
return nil, status.Error(codes.Unimplemented, "document type not implemented")
|
||||
}
|
||||
|
||||
if path, ok := record.StoragePaths[targetType]; ok && path != "" {
|
||||
content, loadErr := s.docStore.Load(ctx, path)
|
||||
if loadErr != nil {
|
||||
return nil, status.Error(codes.Internal, loadErr.Error())
|
||||
}
|
||||
|
||||
return &documentsv1.GetDocumentResponse{
|
||||
Content: content,
|
||||
Filename: documentFilename(docType, paymentRef),
|
||||
MimeType: "application/pdf",
|
||||
}, nil
|
||||
}
|
||||
|
||||
content, hash, genErr := s.generateActPDF(record.Snapshot)
|
||||
snapshot := operationSnapshotFromRequest(req)
|
||||
content, _, genErr := s.generateOperationPDF(snapshot)
|
||||
if genErr != nil {
|
||||
logger.Warn("Failed to generate document", zap.Error(genErr))
|
||||
err = status.Error(codes.Internal, genErr.Error())
|
||||
|
||||
return nil, status.Error(codes.Internal, genErr.Error())
|
||||
}
|
||||
|
||||
path := documentStoragePath(paymentRef, docType)
|
||||
if saveErr := s.docStore.Save(ctx, path, content); saveErr != nil {
|
||||
logger.Warn("Failed to store document", zap.Error(saveErr))
|
||||
|
||||
return nil, status.Error(codes.Internal, saveErr.Error())
|
||||
}
|
||||
|
||||
record.StoragePaths[targetType] = path
|
||||
record.Hashes[targetType] = hash
|
||||
|
||||
if updateErr := s.storage.Documents().Update(ctx, record); updateErr != nil {
|
||||
logger.Warn("Failed to update document record", zap.Error(updateErr))
|
||||
|
||||
return nil, status.Error(codes.Internal, updateErr.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = &documentsv1.GetDocumentResponse{
|
||||
Content: content,
|
||||
Filename: documentFilename(docType, paymentRef),
|
||||
Filename: operationDocumentFilename(operationRef),
|
||||
MimeType: "application/pdf",
|
||||
}
|
||||
|
||||
@@ -391,8 +238,8 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
}
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "BILLING_DOCUMENTS",
|
||||
Operations: []string{discovery.OperationDocumentsBatchResolve, discovery.OperationDocumentsGet},
|
||||
Service: mservice.BillingDocuments,
|
||||
Operations: []string{discovery.OperationDocumentsGet},
|
||||
InvokeURI: s.invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
@@ -400,28 +247,25 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
s.announcer.Start()
|
||||
}
|
||||
|
||||
type serviceError string
|
||||
|
||||
func (e serviceError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
var (
|
||||
errStorageUnavailable = serviceError("documents: storage not initialised")
|
||||
errDocStoreUnavailable = serviceError("documents: document store not initialised")
|
||||
errTemplateUnavailable = serviceError("documents: template renderer not initialised")
|
||||
)
|
||||
|
||||
func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, error) {
|
||||
blocks, err := s.template.Render(snapshot)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return s.renderPDFWithIntegrity(blocks)
|
||||
}
|
||||
|
||||
func (s *Service) generateOperationPDF(snapshot operationSnapshot) ([]byte, string, error) {
|
||||
return s.renderPDFWithIntegrity(buildOperationBlocks(snapshot))
|
||||
}
|
||||
|
||||
func (s *Service) renderPDFWithIntegrity(blocks []renderer.Block) ([]byte, string, error) {
|
||||
generated := renderer.Renderer{
|
||||
Issuer: s.config.Issuer,
|
||||
Issuer: s.config.IssuerDetails(),
|
||||
OwnerPassword: s.config.Protection.OwnerPassword,
|
||||
}
|
||||
|
||||
placeholder := strings.Repeat("0", 64)
|
||||
|
||||
firstPass, err := generated.Render(blocks, placeholder)
|
||||
@@ -440,49 +284,155 @@ func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, er
|
||||
return finalBytes, footerHex, nil
|
||||
}
|
||||
|
||||
func toProtoTypes(types []model.DocumentType) []documentsv1.DocumentType {
|
||||
if len(types) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]documentsv1.DocumentType, 0, len(types))
|
||||
for _, t := range types {
|
||||
result = append(result, t.Proto())
|
||||
}
|
||||
|
||||
return result
|
||||
type operationSnapshot struct {
|
||||
OrganizationRef string
|
||||
GatewayService string
|
||||
OperationRef string
|
||||
PaymentRef string
|
||||
OperationCode string
|
||||
OperationLabel string
|
||||
OperationState string
|
||||
FailureCode string
|
||||
FailureReason string
|
||||
Amount string
|
||||
Currency string
|
||||
StartedAt time.Time
|
||||
CompletedAt time.Time
|
||||
}
|
||||
|
||||
func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string {
|
||||
suffix := "document.pdf"
|
||||
|
||||
switch docType {
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
|
||||
suffix = "act.pdf"
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE:
|
||||
suffix = "invoice.pdf"
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
|
||||
suffix = "receipt.pdf"
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
|
||||
// default suffix used
|
||||
func operationSnapshotFromRequest(req *documentsv1.GetOperationDocumentRequest) operationSnapshot {
|
||||
snapshot := operationSnapshot{
|
||||
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
|
||||
GatewayService: strings.TrimSpace(req.GetGatewayService()),
|
||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
|
||||
OperationCode: strings.TrimSpace(req.GetOperationCode()),
|
||||
OperationLabel: strings.TrimSpace(req.GetOperationLabel()),
|
||||
OperationState: strings.TrimSpace(req.GetOperationState()),
|
||||
FailureCode: strings.TrimSpace(req.GetFailureCode()),
|
||||
FailureReason: strings.TrimSpace(req.GetFailureReason()),
|
||||
Amount: strings.TrimSpace(req.GetAmount()),
|
||||
Currency: strings.TrimSpace(req.GetCurrency()),
|
||||
}
|
||||
|
||||
return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix))
|
||||
}
|
||||
|
||||
func documentFilename(docType documentsv1.DocumentType, paymentRef string) string {
|
||||
name := "document"
|
||||
|
||||
switch docType {
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
|
||||
name = "act"
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE:
|
||||
name = "invoice"
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
|
||||
name = "receipt"
|
||||
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
|
||||
// default name used
|
||||
if ts := req.GetStartedAtUnixMs(); ts > 0 {
|
||||
snapshot.StartedAt = time.UnixMilli(ts).UTC()
|
||||
}
|
||||
if ts := req.GetCompletedAtUnixMs(); ts > 0 {
|
||||
snapshot.CompletedAt = time.UnixMilli(ts).UTC()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s_%s.pdf", name, paymentRef)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
|
||||
documentCopy := content.OperationDocument
|
||||
|
||||
rows := [][]string{
|
||||
{documentCopy.RowOrganization, snapshot.OrganizationRef},
|
||||
{documentCopy.RowGatewayService, snapshot.GatewayService},
|
||||
{documentCopy.RowOperationRef, snapshot.OperationRef},
|
||||
{documentCopy.RowPaymentRef, safeValue(snapshot.PaymentRef)},
|
||||
{documentCopy.RowCode, safeValue(snapshot.OperationCode)},
|
||||
{documentCopy.RowState, safeValue(snapshot.OperationState)},
|
||||
{documentCopy.RowLabel, safeValue(snapshot.OperationLabel)},
|
||||
{documentCopy.RowStartedAtUTC, formatSnapshotTime(snapshot.StartedAt)},
|
||||
{documentCopy.RowCompletedAtUTC, formatSnapshotTime(snapshot.CompletedAt)},
|
||||
}
|
||||
if snapshot.Amount != "" || snapshot.Currency != "" {
|
||||
rows = append(rows, []string{documentCopy.RowAmount, strings.TrimSpace(strings.TrimSpace(snapshot.Amount) + " " + strings.TrimSpace(snapshot.Currency))})
|
||||
}
|
||||
|
||||
blocks := []renderer.Block{
|
||||
{
|
||||
Tag: renderer.TagTitle,
|
||||
Lines: []string{documentCopy.Title},
|
||||
},
|
||||
{
|
||||
Tag: renderer.TagSubtitle,
|
||||
Lines: []string{documentCopy.Subtitle},
|
||||
},
|
||||
{
|
||||
Tag: renderer.TagMeta,
|
||||
Lines: []string{
|
||||
documentCopy.MetaDocumentType,
|
||||
},
|
||||
},
|
||||
{
|
||||
Tag: renderer.TagSection,
|
||||
Lines: []string{documentCopy.SectionOperation},
|
||||
},
|
||||
{
|
||||
Tag: renderer.TagKV,
|
||||
Rows: rows,
|
||||
},
|
||||
}
|
||||
|
||||
if snapshot.FailureCode != "" || snapshot.FailureReason != "" {
|
||||
blocks = append(blocks,
|
||||
renderer.Block{Tag: renderer.TagSection, Lines: []string{documentCopy.SectionFailure}},
|
||||
renderer.Block{
|
||||
Tag: renderer.TagKV,
|
||||
Rows: [][]string{
|
||||
{documentCopy.RowFailureCode, safeValue(snapshot.FailureCode)},
|
||||
{documentCopy.RowFailureReason, safeValue(snapshot.FailureReason)},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
func formatSnapshotTime(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
return content.OperationDocument.MissingValuePlaceholder
|
||||
}
|
||||
|
||||
return value.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func safeValue(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return content.OperationDocument.MissingValuePlaceholder
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func operationDocumentFilename(operationRef string) string {
|
||||
clean := sanitizeFilenameComponent(operationRef)
|
||||
if clean == "" {
|
||||
clean = "operation"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("operation_%s.pdf", clean)
|
||||
}
|
||||
|
||||
func sanitizeFilenameComponent(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(len(trimmed))
|
||||
|
||||
for _, r := range trimmed {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
b.WriteRune(r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
b.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
b.WriteRune(r)
|
||||
case r == '-', r == '_':
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteRune('_')
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Trim(b.String(), "_")
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/billing/documents/internal/content"
|
||||
"github.com/tech/sendico/billing/documents/renderer"
|
||||
"github.com/tech/sendico/billing/documents/storage"
|
||||
"github.com/tech/sendico/billing/documents/storage/model"
|
||||
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type stubRepo struct {
|
||||
@@ -51,38 +54,6 @@ func (s *stubDocumentsStore) ListByPaymentRefs(_ context.Context, _ []string) ([
|
||||
|
||||
var _ storage.DocumentsStore = (*stubDocumentsStore)(nil)
|
||||
|
||||
type memDocStore struct {
|
||||
data map[string][]byte
|
||||
saveCount int
|
||||
loadCount int
|
||||
}
|
||||
|
||||
func newMemDocStore() *memDocStore {
|
||||
return &memDocStore{data: map[string][]byte{}}
|
||||
}
|
||||
|
||||
func (m *memDocStore) Save(_ context.Context, key string, data []byte) error {
|
||||
m.saveCount++
|
||||
copyData := make([]byte, len(data))
|
||||
copy(copyData, data)
|
||||
m.data[key] = copyData
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memDocStore) Load(_ context.Context, key string) ([]byte, error) {
|
||||
m.loadCount++
|
||||
data := m.data[key]
|
||||
copyData := make([]byte, len(data))
|
||||
copy(copyData, data)
|
||||
|
||||
return copyData, nil
|
||||
}
|
||||
|
||||
func (m *memDocStore) Counts() (int, int) {
|
||||
return m.saveCount, m.loadCount
|
||||
}
|
||||
|
||||
type stubTemplate struct {
|
||||
blocks []renderer.Block
|
||||
calls int
|
||||
@@ -94,9 +65,7 @@ func (s *stubTemplate) Render(_ model.ActSnapshot) ([]renderer.Block, error) {
|
||||
return s.blocks, nil
|
||||
}
|
||||
|
||||
func TestGetDocument_IdempotentAndHashed(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
func TestGenerateActPDF_IdempotentAndHashed(t *testing.T) {
|
||||
snapshot := model.ActSnapshot{
|
||||
PaymentID: "PAY-123",
|
||||
Date: time.Date(2026, 1, 30, 0, 0, 0, 0, time.UTC),
|
||||
@@ -105,14 +74,6 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
|
||||
Currency: "USD",
|
||||
}
|
||||
|
||||
record := &model.DocumentRecord{
|
||||
PaymentRef: "PAY-123",
|
||||
Snapshot: snapshot,
|
||||
}
|
||||
|
||||
documentsStore := &stubDocumentsStore{record: record}
|
||||
repo := &stubRepo{store: documentsStore}
|
||||
store := newMemDocStore()
|
||||
tmpl := &stubTemplate{
|
||||
blocks: []renderer.Block{
|
||||
{Tag: renderer.TagTitle, Lines: []string{"ACT"}},
|
||||
@@ -120,74 +81,51 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Issuer: renderer.Issuer{
|
||||
LegalName: "Sendico Ltd",
|
||||
LegalAddress: "12 Market Street, London, UK",
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), repo, nil,
|
||||
WithConfig(cfg),
|
||||
WithDocumentStore(store),
|
||||
svc := NewService(zap.NewNop(), nil, nil,
|
||||
WithTemplateRenderer(tmpl),
|
||||
)
|
||||
|
||||
resp1, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{
|
||||
PaymentRef: "PAY-123",
|
||||
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
|
||||
})
|
||||
pdf1, hash1, err := svc.generateActPDF(snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDocument first call: %v", err)
|
||||
t.Fatalf("generateActPDF first call: %v", err)
|
||||
}
|
||||
|
||||
if len(resp1.GetContent()) == 0 {
|
||||
if len(pdf1) == 0 {
|
||||
t.Fatalf("expected content on first call")
|
||||
}
|
||||
|
||||
stored := record.Hashes[model.DocumentTypeAct]
|
||||
|
||||
if stored == "" {
|
||||
t.Fatalf("expected stored hash")
|
||||
if hash1 == "" {
|
||||
t.Fatalf("expected non-empty hash on first call")
|
||||
}
|
||||
|
||||
footerHash := extractFooterHash(resp1.GetContent())
|
||||
footerHash := extractFooterHash(pdf1)
|
||||
|
||||
if footerHash == "" {
|
||||
t.Fatalf("expected footer hash in PDF")
|
||||
}
|
||||
|
||||
if stored != footerHash {
|
||||
t.Fatalf("stored hash mismatch: got %s", stored)
|
||||
if hash1 != footerHash {
|
||||
t.Fatalf("stored hash mismatch: got %s", hash1)
|
||||
}
|
||||
|
||||
resp2, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{
|
||||
PaymentRef: "PAY-123",
|
||||
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
|
||||
})
|
||||
pdf2, hash2, err := svc.generateActPDF(snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDocument second call: %v", err)
|
||||
t.Fatalf("generateActPDF second call: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(resp1.GetContent(), resp2.GetContent()) {
|
||||
t.Fatalf("expected identical PDF bytes on second call")
|
||||
if hash2 == "" {
|
||||
t.Fatalf("expected non-empty hash on second call")
|
||||
}
|
||||
|
||||
if tmpl.calls != 1 {
|
||||
t.Fatalf("expected template to be rendered once, got %d", tmpl.calls)
|
||||
footerHash2 := extractFooterHash(pdf2)
|
||||
if footerHash2 == "" {
|
||||
t.Fatalf("expected footer hash in second PDF")
|
||||
}
|
||||
|
||||
if store.saveCount != 1 {
|
||||
t.Fatalf("expected document save once, got %d", store.saveCount)
|
||||
}
|
||||
|
||||
if store.loadCount == 0 {
|
||||
t.Fatalf("expected document load on second call")
|
||||
if footerHash2 != hash2 {
|
||||
t.Fatalf("second hash mismatch: got=%s want=%s", footerHash2, hash2)
|
||||
}
|
||||
}
|
||||
|
||||
func extractFooterHash(pdf []byte) string {
|
||||
prefix := []byte("Document integrity hash: ")
|
||||
prefix := []byte(content.DocumentIntegrityHashPrefix)
|
||||
idx := bytes.Index(pdf, prefix)
|
||||
|
||||
if idx == -1 {
|
||||
@@ -212,3 +150,44 @@ func extractFooterHash(pdf []byte) string {
|
||||
func isHexDigit(b byte) bool {
|
||||
return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')
|
||||
}
|
||||
|
||||
func TestGetOperationDocument_GeneratesPDF(t *testing.T) {
|
||||
svc := NewService(zap.NewNop(), nil, nil)
|
||||
|
||||
resp, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
|
||||
OrganizationRef: "org-1",
|
||||
GatewayService: "chain_gateway",
|
||||
OperationRef: "pay-1:step-1",
|
||||
PaymentRef: "pay-1",
|
||||
OperationCode: "crypto.transfer",
|
||||
OperationLabel: "Outbound transfer",
|
||||
OperationState: "completed",
|
||||
Amount: "100.50",
|
||||
Currency: "USDT",
|
||||
StartedAtUnixMs: time.Date(2026, 3, 4, 10, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetOperationDocument failed: %v", err)
|
||||
}
|
||||
if len(resp.GetContent()) == 0 {
|
||||
t.Fatalf("expected non-empty PDF content")
|
||||
}
|
||||
if got, want := resp.GetMimeType(), "application/pdf"; got != want {
|
||||
t.Fatalf("mime_type mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := resp.GetFilename(), "operation_pay-1_step-1.pdf"; got != want {
|
||||
t.Fatalf("filename mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOperationDocument_RequiresOperationRef(t *testing.T) {
|
||||
svc := NewService(zap.NewNop(), nil, nil)
|
||||
|
||||
_, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
|
||||
OrganizationRef: "org-1",
|
||||
GatewayService: "chain_gateway",
|
||||
})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("expected InvalidArgument, got=%v err=%v", status.Code(err), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/billing/documents/internal/content"
|
||||
"github.com/tech/sendico/billing/documents/renderer"
|
||||
"github.com/tech/sendico/billing/documents/storage/model"
|
||||
)
|
||||
@@ -17,7 +18,13 @@ type templateRenderer struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
type acceptanceTemplateData struct {
|
||||
model.ActSnapshot
|
||||
Content content.AcceptanceTemplateContent
|
||||
}
|
||||
|
||||
func newTemplateRenderer(path string) (*templateRenderer, error) {
|
||||
//nolint:gosec // template file path is provided by trusted service configuration.
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read template: %w", err)
|
||||
@@ -38,7 +45,12 @@ func newTemplateRenderer(path string) (*templateRenderer, error) {
|
||||
|
||||
func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := r.tpl.Execute(&buf, snapshot); err != nil {
|
||||
data := acceptanceTemplateData{
|
||||
ActSnapshot: snapshot,
|
||||
Content: content.AcceptanceTemplate,
|
||||
}
|
||||
|
||||
if err := r.tpl.Execute(&buf, data); err != nil {
|
||||
return nil, fmt.Errorf("execute template: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/billing/documents/internal/content"
|
||||
"github.com/tech/sendico/billing/documents/renderer"
|
||||
"github.com/tech/sendico/billing/documents/storage/model"
|
||||
)
|
||||
@@ -42,7 +43,7 @@ func TestTemplateRenderer_Render(t *testing.T) {
|
||||
t.Fatalf("expected title block")
|
||||
}
|
||||
|
||||
if !slices.Contains(title.Lines, "ACT OF ACCEPTANCE OF SERVICES") {
|
||||
if !slices.Contains(title.Lines, content.AcceptanceTemplate.Title) {
|
||||
t.Fatalf("expected title content not found")
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ func TestTemplateRenderer_Render(t *testing.T) {
|
||||
foundExecutor := false
|
||||
|
||||
for _, row := range kv.Rows {
|
||||
if len(row) >= 2 && row[0] == "Executor" && row[1] == snapshot.ExecutorFullName {
|
||||
if len(row) >= 2 && row[0] == content.AcceptanceTemplate.PartyExecutorLabel && row[1] == snapshot.ExecutorFullName {
|
||||
foundExecutor = true
|
||||
|
||||
break
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
"github.com/tech/sendico/billing/documents/internal/content"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,7 +29,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
|
||||
pdf.SetAutoPageBreak(true, pageMarginBottom)
|
||||
pdf.SetCompression(false)
|
||||
pdf.SetAuthor(r.Issuer.LegalName, false)
|
||||
pdf.SetTitle("Act of Acceptance", false)
|
||||
pdf.SetTitle(content.PDFTitleActOfAcceptance, false)
|
||||
|
||||
owner := strings.TrimSpace(r.OwnerPassword)
|
||||
if owner != "" {
|
||||
@@ -39,7 +40,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
|
||||
pdf.SetY(-15)
|
||||
pdf.SetFont("Helvetica", "", 8)
|
||||
|
||||
footer := "Document integrity hash: " + footerHash
|
||||
footer := content.DocumentIntegrityHashPrefix + footerHash
|
||||
pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "")
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/tech/sendico/billing/documents/internal/content"
|
||||
)
|
||||
|
||||
func TestRenderer_RenderContainsText(t *testing.T) {
|
||||
@@ -31,7 +33,7 @@ func TestRenderer_RenderContainsText(t *testing.T) {
|
||||
t.Fatalf("expected PDF bytes")
|
||||
}
|
||||
|
||||
checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", "Document integrity hash"}
|
||||
checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", strings.TrimSpace(strings.TrimSuffix(content.DocumentIntegrityHashPrefix, ": "))}
|
||||
|
||||
for _, token := range checks {
|
||||
if !containsPDFText(pdfBytes, token) {
|
||||
@@ -100,7 +102,7 @@ func encodeUTF16BE(text string, withBOM bool) []byte {
|
||||
}
|
||||
|
||||
for _, v := range encoded {
|
||||
out = append(out, byte(v>>8), byte(v))
|
||||
out = append(out, byte(v>>8), byte(v&0x00FF))
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
@@ -6,14 +6,13 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
DocumentRecordsCollection = "document_records"
|
||||
)
|
||||
|
||||
// DocumentType mirrors the protobuf enum but stores string names for Mongo compatibility.
|
||||
// DocumentType represents document kinds cached in storage.
|
||||
type DocumentType string
|
||||
|
||||
const (
|
||||
@@ -23,24 +22,6 @@ const (
|
||||
DocumentTypeReceipt DocumentType = "DOCUMENT_TYPE_RECEIPT"
|
||||
)
|
||||
|
||||
// DocumentTypeFromProto converts a protobuf enum to the storage representation.
|
||||
func DocumentTypeFromProto(t documentsv1.DocumentType) DocumentType {
|
||||
if name, ok := documentsv1.DocumentType_name[int32(t)]; ok {
|
||||
return DocumentType(name)
|
||||
}
|
||||
|
||||
return DocumentTypeUnspecified
|
||||
}
|
||||
|
||||
// Proto converts the storage representation to a protobuf enum.
|
||||
func (t DocumentType) Proto() documentsv1.DocumentType {
|
||||
if value, ok := documentsv1.DocumentType_value[string(t)]; ok {
|
||||
return documentsv1.DocumentType(value)
|
||||
}
|
||||
|
||||
return documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
|
||||
}
|
||||
|
||||
// ActSnapshot captures the immutable data needed to generate an acceptance act.
|
||||
type ActSnapshot struct {
|
||||
PaymentID string `bson:"paymentId" json:"paymentId"`
|
||||
|
||||
@@ -2,66 +2,66 @@
|
||||
|
||||
|
||||
#title
|
||||
ACT OF ACCEPTANCE OF SERVICES
|
||||
{{ .Content.Title }}
|
||||
|
||||
#subtitle
|
||||
under the Public Offer Agreement
|
||||
{{ .Content.Subtitle }}
|
||||
|
||||
#meta
|
||||
Date: {{ date .Date }}
|
||||
Act No: {{ .PaymentID }}
|
||||
{{ .Content.MetaDateLabel }}: {{ date .Date }}
|
||||
{{ .Content.MetaActNumberLabel }}: {{ .PaymentID }}
|
||||
|
||||
|
||||
#section
|
||||
PARTIES
|
||||
{{ .Content.SectionParties }}
|
||||
|
||||
#text
|
||||
This Act is made between the following Parties.
|
||||
{{ .Content.PartiesIntro }}
|
||||
|
||||
#kv
|
||||
Executor | {{ .ExecutorFullName }}
|
||||
Status | Individual
|
||||
{{ .Content.PartyExecutorLabel }} | {{ .ExecutorFullName }}
|
||||
{{ .Content.PartyStatusLabel }} | {{ .Content.PartyStatusValue }}
|
||||
|
||||
|
||||
#section
|
||||
BASIS
|
||||
{{ .Content.SectionBasis }}
|
||||
|
||||
#text
|
||||
This Act is issued pursuant to the Public Offer Agreement
|
||||
accepted by the Executor by joining the offer.
|
||||
{{ .Content.BasisLine1 }}
|
||||
{{ .Content.BasisLine2 }}
|
||||
|
||||
|
||||
#section
|
||||
SERVICES RENDERED
|
||||
{{ .Content.SectionServicesRendered }}
|
||||
|
||||
#text
|
||||
The Executor has rendered services to the Customer
|
||||
in accordance with the terms of the Public Offer Agreement.
|
||||
{{ .Content.ServicesRenderedLine1 }}
|
||||
{{ .Content.ServicesRenderedLine2 }}
|
||||
|
||||
|
||||
#section
|
||||
REMUNERATION
|
||||
{{ .Content.SectionRemuneration }}
|
||||
|
||||
#table
|
||||
Description | Amount
|
||||
Services rendered under the Public Offer Agreement | {{ money .Amount .Currency }}
|
||||
{{ .Content.RemunerationHeaderDesc }} | {{ .Content.RemunerationHeaderAmount }}
|
||||
{{ .Content.RemunerationServicesDesc }} | {{ money .Amount .Currency }}
|
||||
|
||||
|
||||
#section
|
||||
CONFIRMATION
|
||||
{{ .Content.SectionConfirmation }}
|
||||
|
||||
#text
|
||||
The Customer confirms that the services were rendered properly
|
||||
and accepted without any claims.
|
||||
{{ .Content.ConfirmationLine1 }}
|
||||
{{ .Content.ConfirmationLine2 }}
|
||||
|
||||
The remuneration for the services was paid to the Executor
|
||||
using the bank card details provided by the Executor.
|
||||
{{ .Content.ConfirmationPaymentLine1 }}
|
||||
{{ .Content.ConfirmationPaymentLine2 }}
|
||||
|
||||
|
||||
#section
|
||||
SIGNATURES
|
||||
{{ .Content.SectionSignatures }}
|
||||
|
||||
#sign
|
||||
Customer ___________________________
|
||||
{{ .Content.SignatureCustomerLine }}
|
||||
|
||||
Executor ___________________________
|
||||
{{ .Content.SignatureExecutorLine }}
|
||||
|
||||
@@ -1,198 +1,47 @@
|
||||
# See the dedicated "version" documentation section.
|
||||
version: "2"
|
||||
linters:
|
||||
# Default set of linters.
|
||||
# The value can be:
|
||||
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
|
||||
# - `all`: enables all linters by default.
|
||||
# - `none`: disables all linters by default.
|
||||
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
|
||||
# Default: standard
|
||||
default: all
|
||||
# Enable specific linter.
|
||||
default: none
|
||||
enable:
|
||||
- arangolint
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- cyclop
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- embeddedstructfieldcheck
|
||||
- err113
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exptostd
|
||||
- fatcontext
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funcorder
|
||||
- funlen
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gochecksumtype
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godoclint
|
||||
- godot
|
||||
- godox
|
||||
- goheader
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- govet
|
||||
- grouper
|
||||
- iface
|
||||
- importas
|
||||
- inamedparam
|
||||
- ineffassign
|
||||
- interfacebloat
|
||||
- intrange
|
||||
- iotamixing
|
||||
- ireturn
|
||||
- lll
|
||||
- loggercheck
|
||||
- maintidx
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- modernize
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnesserr
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- noctx
|
||||
- noinlineerr
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
- recvcheck
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- spancheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- tagalign
|
||||
- tagliatelle
|
||||
- testableexamples
|
||||
- testifylint
|
||||
- testpackage
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- unqueryvet
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- varnamelen
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- wsl_v5
|
||||
- zerologlint
|
||||
# Disable specific linters.
|
||||
disable:
|
||||
- depguard
|
||||
- exhaustruct
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gomoddirectives
|
||||
- noinlineerr
|
||||
- wsl
|
||||
- wrapcheck
|
||||
# All available settings of specific linters.
|
||||
# See the dedicated "linters.settings" documentation section.
|
||||
settings:
|
||||
wsl_v5:
|
||||
allow-first-in-block: true
|
||||
allow-whole-block: false
|
||||
branch-max-lines: 2
|
||||
|
||||
# Defines a set of rules to ignore issues.
|
||||
# It does not skip the analysis, and so does not ignore "typecheck" errors.
|
||||
exclusions:
|
||||
# Mode of the generated files analysis.
|
||||
#
|
||||
# - `strict`: sources are excluded by strictly following the Go generated file convention.
|
||||
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
|
||||
# This line must appear before the first non-comment, non-blank text in the file.
|
||||
# https://go.dev/s/generatedcode
|
||||
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
|
||||
# - `disable`: disable the generated files exclusion.
|
||||
#
|
||||
# Default: strict
|
||||
generated: lax
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
# Predefined exclusion rules.
|
||||
# Default: []
|
||||
presets:
|
||||
- comments
|
||||
- std-error-handling
|
||||
- common-false-positives
|
||||
- legacy
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source.
|
||||
rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- cyclop
|
||||
- funlen
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
# Run some linter only for test files by excluding its issues for everything else.
|
||||
- path-except: _test\.go
|
||||
linters:
|
||||
- forbidigo
|
||||
# Exclude known linters from partially hard-vendored code,
|
||||
# which is impossible to exclude via `nolint` comments.
|
||||
# `/` will be replaced by the current OS file path separator to properly work on Windows.
|
||||
- path: internal/hmac/
|
||||
text: "weak cryptographic primitive"
|
||||
linters:
|
||||
- gosec
|
||||
# Exclude some `staticcheck` messages.
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA9003:"
|
||||
# Exclude `lll` issues for long lines with `go:generate`.
|
||||
- linters:
|
||||
- lll
|
||||
source: "^//go:generate "
|
||||
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
|
||||
# "/" will be replaced by the current OS file path separator to properly work on Windows.
|
||||
# Default: []
|
||||
paths: []
|
||||
# Which file paths to not exclude.
|
||||
# Default: []
|
||||
paths-except: []
|
||||
- cyclop
|
||||
- dupl
|
||||
- funlen
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- ireturn
|
||||
- lll
|
||||
- mnd
|
||||
- nestif
|
||||
- nlreturn
|
||||
- noinlineerr
|
||||
- paralleltest
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wsl_v5
|
||||
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
github.com/tech/sendico/fx/oracle v0.0.0
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
go.uber.org/zap v1.27.1
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/grpc v1.79.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -38,17 +38,17 @@ require (
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/protobuf v1.36.11
|
||||
|
||||
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
@@ -168,8 +168,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
@@ -183,16 +183,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -210,8 +210,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -385,7 +386,7 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
|
||||
|
||||
logFields := []zap.Field{zap.Time("booked_at_used", bookedAt)}
|
||||
if !orgRef.IsZero() {
|
||||
logFields = append(logFields, zap.String("organization_ref", orgRef.Hex()))
|
||||
logFields = append(logFields, mzap.ObjRef("organization_ref", orgRef))
|
||||
}
|
||||
|
||||
logFields = append(logFields, logFieldsFromIntent(intent)...)
|
||||
@@ -563,7 +564,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
}
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "BILLING_FEES",
|
||||
Service: mservice.BillingFees,
|
||||
Operations: []string{discovery.OperationFeeCalc},
|
||||
InvokeURI: s.invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
|
||||
@@ -1,196 +1,47 @@
|
||||
# See the dedicated "version" documentation section.
|
||||
version: "2"
|
||||
linters:
|
||||
# Default set of linters.
|
||||
# The value can be:
|
||||
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
|
||||
# - `all`: enables all linters by default.
|
||||
# - `none`: disables all linters by default.
|
||||
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
|
||||
# Default: standard
|
||||
default: all
|
||||
# Enable specific linter.
|
||||
default: none
|
||||
enable:
|
||||
- arangolint
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- cyclop
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- dupword
|
||||
- durationcheck
|
||||
- embeddedstructfieldcheck
|
||||
- err113
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exptostd
|
||||
- fatcontext
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funcorder
|
||||
- funlen
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gochecksumtype
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godoclint
|
||||
- godot
|
||||
- godox
|
||||
- goheader
|
||||
- gomodguard
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- govet
|
||||
- grouper
|
||||
- iface
|
||||
- importas
|
||||
- inamedparam
|
||||
- ineffassign
|
||||
- interfacebloat
|
||||
- intrange
|
||||
- iotamixing
|
||||
- ireturn
|
||||
- lll
|
||||
- loggercheck
|
||||
- maintidx
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- modernize
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnesserr
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- noctx
|
||||
- noinlineerr
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
- recvcheck
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- spancheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- tagalign
|
||||
- tagliatelle
|
||||
- testableexamples
|
||||
- testifylint
|
||||
- testpackage
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- unqueryvet
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- usetesting
|
||||
- varnamelen
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- wsl_v5
|
||||
- zerologlint
|
||||
# Disable specific linters.
|
||||
disable:
|
||||
disable:
|
||||
- depguard
|
||||
- exhaustruct
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gomoddirectives
|
||||
- wsl
|
||||
- wrapcheck
|
||||
# All available settings of specific linters.
|
||||
# See the dedicated "linters.settings" documentation section.
|
||||
settings:
|
||||
wsl_v5:
|
||||
allow-first-in-block: true
|
||||
allow-whole-block: false
|
||||
branch-max-lines: 2
|
||||
|
||||
# Defines a set of rules to ignore issues.
|
||||
# It does not skip the analysis, and so does not ignore "typecheck" errors.
|
||||
exclusions:
|
||||
# Mode of the generated files analysis.
|
||||
#
|
||||
# - `strict`: sources are excluded by strictly following the Go generated file convention.
|
||||
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
|
||||
# This line must appear before the first non-comment, non-blank text in the file.
|
||||
# https://go.dev/s/generatedcode
|
||||
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
|
||||
# - `disable`: disable the generated files exclusion.
|
||||
#
|
||||
# Default: strict
|
||||
generated: lax
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
warn-unused: true
|
||||
# Predefined exclusion rules.
|
||||
# Default: []
|
||||
presets:
|
||||
- comments
|
||||
- std-error-handling
|
||||
- common-false-positives
|
||||
- legacy
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source.
|
||||
rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- funlen
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
# Run some linter only for test files by excluding its issues for everything else.
|
||||
- path-except: _test\.go
|
||||
linters:
|
||||
- forbidigo
|
||||
# Exclude known linters from partially hard-vendored code,
|
||||
# which is impossible to exclude via `nolint` comments.
|
||||
# `/` will be replaced by the current OS file path separator to properly work on Windows.
|
||||
- path: internal/hmac/
|
||||
text: "weak cryptographic primitive"
|
||||
linters:
|
||||
- gosec
|
||||
# Exclude some `staticcheck` messages.
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA9003:"
|
||||
# Exclude `lll` issues for long lines with `go:generate`.
|
||||
- linters:
|
||||
- lll
|
||||
source: "^//go:generate "
|
||||
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
|
||||
# "/" will be replaced by the current OS file path separator to properly work on Windows.
|
||||
# Default: []
|
||||
paths: []
|
||||
# Which file paths to not exclude.
|
||||
# Default: []
|
||||
paths-except: []
|
||||
- cyclop
|
||||
- dupl
|
||||
- funlen
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- ireturn
|
||||
- lll
|
||||
- mnd
|
||||
- nestif
|
||||
- nlreturn
|
||||
- noinlineerr
|
||||
- paralleltest
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wsl_v5
|
||||
|
||||
@@ -30,20 +30,20 @@ require (
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/grpc v1.79.1 // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
@@ -168,8 +168,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
@@ -183,16 +183,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -210,8 +210,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -49,7 +49,7 @@ func (i *Imp) startDiscovery(cfg *config) error {
|
||||
i.registrySvc = svc
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "DISCOVERY",
|
||||
Service: mservice.Discovery,
|
||||
InstanceID: discovery.InstanceID(),
|
||||
Operations: []string{discovery.OperationDiscoveryLookup},
|
||||
Version: appversion.Create().Short(),
|
||||
|
||||
47
api/edge/bff/.golangci.yml
Normal file
47
api/edge/bff/.golangci.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- bodyclose
|
||||
- canonicalheader
|
||||
- copyloopvar
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- nilerr
|
||||
- nilnesserr
|
||||
- nilnil
|
||||
- noctx
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- wastedassign
|
||||
disable:
|
||||
- depguard
|
||||
- exhaustruct
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gomoddirectives
|
||||
- wrapcheck
|
||||
- cyclop
|
||||
- dupl
|
||||
- funlen
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- ireturn
|
||||
- lll
|
||||
- mnd
|
||||
- nestif
|
||||
- nlreturn
|
||||
- noinlineerr
|
||||
- paralleltest
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wsl_v5
|
||||
@@ -109,6 +109,19 @@ api:
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
callbacks:
|
||||
default_event_types:
|
||||
- payment.status.updated
|
||||
default_status: active
|
||||
secret_path_prefix: sendico/callbacks
|
||||
secret_field: value
|
||||
secret_length_bytes: 32
|
||||
vault:
|
||||
address: "http://dev-vault:8200"
|
||||
token_env: VAULT_TOKEN
|
||||
token_file_env: VAULT_TOKEN_FILE
|
||||
namespace: ""
|
||||
mount_path: kv
|
||||
|
||||
app:
|
||||
|
||||
|
||||
@@ -111,6 +111,19 @@ api:
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 5
|
||||
insecure: true
|
||||
callbacks:
|
||||
default_event_types:
|
||||
- payment.status.updated
|
||||
default_status: active
|
||||
secret_path_prefix: sendico/callbacks
|
||||
secret_field: value
|
||||
secret_length_bytes: 32
|
||||
vault:
|
||||
address: "https://vault.sendico.io"
|
||||
token_env: VAULT_TOKEN
|
||||
token_file_env: VAULT_TOKEN_FILE
|
||||
namespace: ""
|
||||
mount_path: kv
|
||||
|
||||
app:
|
||||
|
||||
|
||||
@@ -15,16 +15,17 @@ replace github.com/tech/sendico/payments/storage => ../../payments/storage
|
||||
replace github.com/tech/sendico/gateway/tron => ../../gateway/tron
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-chi/jwtauth/v5 v5.4.0
|
||||
github.com/go-chi/metrics v0.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tech/sendico/gateway/tron v0.0.0-00010101000000-000000000000
|
||||
@@ -37,7 +38,7 @@ require (
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/net v0.51.0
|
||||
google.golang.org/grpc v1.79.1
|
||||
google.golang.org/grpc v1.79.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
moul.io/chizap v1.0.3
|
||||
@@ -53,20 +54,20 @@ require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
|
||||
@@ -83,11 +84,22 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-chi/chi v1.5.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/hashicorp/vault/api v1.22.0 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||
@@ -100,6 +112,7 @@ require (
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // 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.6.0 // indirect
|
||||
@@ -116,10 +129,10 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
@@ -134,16 +147,17 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
)
|
||||
|
||||
@@ -6,42 +6,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@@ -85,6 +85,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
||||
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||
@@ -98,6 +100,8 @@ github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTw
|
||||
github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ=
|
||||
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
|
||||
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -106,6 +110,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -123,6 +129,29 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
|
||||
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
|
||||
github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
|
||||
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/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
@@ -159,6 +188,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -204,10 +235,12 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q=
|
||||
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
@@ -263,20 +296,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
@@ -291,8 +324,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -320,8 +353,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -337,8 +370,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
@@ -349,8 +382,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
@@ -370,8 +403,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -159,7 +159,7 @@ func (s *service) VerifyAccount(
|
||||
token, err := s.vdb.Create(
|
||||
ctx,
|
||||
verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, "").
|
||||
WithTTL(time.Duration(time.Hour*24)),
|
||||
WithTTL(time.Hour * 24),
|
||||
)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
|
||||
@@ -238,7 +238,7 @@ func (s *service) ResetPassword(
|
||||
return s.vdb.Create(
|
||||
ctx,
|
||||
verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, "").
|
||||
WithTTL(time.Duration(time.Hour*1)),
|
||||
WithTTL(time.Hour),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ func (s *service) UpdateLogin(
|
||||
return s.vdb.Create(
|
||||
ctx,
|
||||
verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin).
|
||||
WithTTL(time.Duration(time.Hour*1)),
|
||||
WithTTL(time.Hour),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/vault/kv"
|
||||
mwa "github.com/tech/sendico/server/interface/middleware"
|
||||
fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
|
||||
)
|
||||
@@ -13,6 +14,7 @@ type Config struct {
|
||||
PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"`
|
||||
PaymentQuotation *PaymentOrchestratorConfig `yaml:"payment_quotation"`
|
||||
PaymentMethods *PaymentOrchestratorConfig `yaml:"payment_methods"`
|
||||
Callbacks *CallbacksConfig `yaml:"callbacks"`
|
||||
}
|
||||
|
||||
type ChainGatewayConfig struct {
|
||||
@@ -45,3 +47,12 @@ type PaymentOrchestratorConfig struct {
|
||||
CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
|
||||
Insecure bool `yaml:"insecure"`
|
||||
}
|
||||
|
||||
type CallbacksConfig struct {
|
||||
DefaultEventTypes []string `yaml:"default_event_types"`
|
||||
DefaultStatus string `yaml:"default_status"`
|
||||
SecretPathPrefix string `yaml:"secret_path_prefix"`
|
||||
SecretField string `yaml:"secret_field"`
|
||||
SecretLengthBytes int `yaml:"secret_length_bytes"`
|
||||
Vault kv.Config `yaml:"vault"`
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@ func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) {
|
||||
|
||||
func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
|
||||
if old == nil {
|
||||
//nolint:nilnil // Nil legacy endpoint means no endpoint provided.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -202,6 +203,7 @@ func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint,
|
||||
|
||||
func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) {
|
||||
if new == nil {
|
||||
//nolint:nilnil // Nil endpoint DTO means no endpoint provided.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +73,10 @@ func validateQuoteIdempotency(previewOnly bool, idempotencyKey string) error {
|
||||
}
|
||||
|
||||
type InitiatePayment struct {
|
||||
PaymentBase `json:",inline"`
|
||||
Intent *PaymentIntent `json:"intent,omitempty"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
PaymentBase `json:",inline"`
|
||||
Intent *PaymentIntent `json:"intent,omitempty"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
ClientPaymentRef string `json:"clientPaymentRef,omitempty"`
|
||||
}
|
||||
|
||||
func (r InitiatePayment) Validate() error {
|
||||
@@ -106,8 +107,9 @@ func (r InitiatePayment) Validate() error {
|
||||
}
|
||||
|
||||
type InitiatePayments struct {
|
||||
PaymentBase `json:",inline"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
PaymentBase `json:",inline"`
|
||||
QuoteRef string `json:"quoteRef,omitempty"`
|
||||
ClientPaymentRef string `json:"clientPaymentRef,omitempty"`
|
||||
}
|
||||
|
||||
func (r *InitiatePayments) Validate() error {
|
||||
|
||||
@@ -14,6 +14,7 @@ type PaymentIntent struct {
|
||||
SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
|
||||
FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
33
api/edge/bff/interface/api/sresponse/callback.go
Normal file
33
api/edge/bff/interface/api/sresponse/callback.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type callbackWriteResponse struct {
|
||||
AccessToken TokenData `json:"accessToken"`
|
||||
Callbacks []model.Callback `json:"callbacks"`
|
||||
GeneratedSigningSecret string `json:"generatedSigningSecret,omitempty"`
|
||||
}
|
||||
|
||||
func Callback(
|
||||
logger mlogger.Logger,
|
||||
callback *model.Callback,
|
||||
accessToken *TokenData,
|
||||
generatedSecret string,
|
||||
created bool,
|
||||
) http.HandlerFunc {
|
||||
resp := callbackWriteResponse{
|
||||
AccessToken: *accessToken,
|
||||
Callbacks: []model.Callback{*callback},
|
||||
GeneratedSigningSecret: generatedSecret,
|
||||
}
|
||||
if created {
|
||||
return response.Created(logger, resp)
|
||||
}
|
||||
return response.Ok(logger, resp)
|
||||
}
|
||||
@@ -8,12 +8,17 @@ import (
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
@@ -65,26 +70,39 @@ type PaymentQuotes struct {
|
||||
}
|
||||
|
||||
type Payment struct {
|
||||
PaymentRef string `json:"paymentRef,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
Operations []PaymentOperation `json:"operations,omitempty"`
|
||||
LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
PaymentRef string `json:"paymentRef,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Source *PaymentEndpoint `json:"source"`
|
||||
Destination *PaymentEndpoint `json:"destination"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
Operations []PaymentOperation `json:"operations,omitempty"`
|
||||
LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentEndpoint struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
PaymentMethodRef string `json:"paymentMethodRef,omitempty"`
|
||||
PayeeRef string `json:"payeeRef,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentOperation struct {
|
||||
StepRef string `json:"stepRef,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
StepRef string `json:"stepRef,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Amount *paymenttypes.Money `json:"amount,omitempty"`
|
||||
ConvertedAmount *paymenttypes.Money `json:"convertedAmount,omitempty"`
|
||||
OperationRef string `json:"operationRef,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
FailureCode string `json:"failureCode,omitempty"`
|
||||
FailureReason string `json:"failureReason,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type paymentQuoteResponse struct {
|
||||
@@ -283,21 +301,257 @@ func toPayment(p *orchestrationv2.Payment) *Payment {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
intent := p.GetIntentSnapshot()
|
||||
operations := toUserVisibleOperations(p.GetStepExecutions())
|
||||
failureCode, failureReason := firstFailure(operations)
|
||||
return &Payment{
|
||||
PaymentRef: p.GetPaymentRef(),
|
||||
State: enumJSONName(p.GetState().String()),
|
||||
FailureCode: failureCode,
|
||||
FailureReason: failureReason,
|
||||
Operations: operations,
|
||||
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
|
||||
CreatedAt: timestampAsTime(p.GetCreatedAt()),
|
||||
Meta: paymentMeta(p),
|
||||
IdempotencyKey: "",
|
||||
PaymentRef: p.GetPaymentRef(),
|
||||
State: enumJSONName(p.GetState().String()),
|
||||
Comment: strings.TrimSpace(intent.GetComment()),
|
||||
Source: toPaymentEndpoint(intent.GetSource()),
|
||||
Destination: toPaymentEndpoint(intent.GetDestination()),
|
||||
FailureCode: failureCode,
|
||||
FailureReason: failureReason,
|
||||
Operations: operations,
|
||||
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
|
||||
CreatedAt: timestampAsTime(p.GetCreatedAt()),
|
||||
Meta: paymentMeta(p),
|
||||
}
|
||||
}
|
||||
|
||||
func toPaymentEndpoint(endpoint *endpointv1.PaymentEndpoint) *PaymentEndpoint {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
if paymentMethodRef := strings.TrimSpace(endpoint.GetPaymentMethodRef()); paymentMethodRef != "" {
|
||||
return &PaymentEndpoint{PaymentMethodRef: paymentMethodRef}
|
||||
}
|
||||
if payeeRef := strings.TrimSpace(endpoint.GetPayeeRef()); payeeRef != "" {
|
||||
return &PaymentEndpoint{PayeeRef: payeeRef}
|
||||
}
|
||||
method := endpoint.GetPaymentMethod()
|
||||
if method == nil {
|
||||
return nil
|
||||
}
|
||||
return &PaymentEndpoint{
|
||||
Type: paymentEndpointType(method.GetType()),
|
||||
Data: paymentEndpointData(method),
|
||||
}
|
||||
}
|
||||
|
||||
func paymentEndpointType(methodType endpointv1.PaymentMethodType) string {
|
||||
switch methodType {
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
|
||||
return string(srequest.EndpointTypeIBAN)
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
|
||||
return string(srequest.EndpointTypeCard)
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
|
||||
return string(srequest.EndpointTypeCardToken)
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
|
||||
return string(srequest.EndpointTypeBankAccount)
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
|
||||
return string(srequest.EndpointTypeWallet)
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
|
||||
return string(srequest.EndpointTypeExternalChain)
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
|
||||
return string(srequest.EndpointTypeLedger)
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_ACCOUNT:
|
||||
return "account"
|
||||
default:
|
||||
return "unspecified"
|
||||
}
|
||||
}
|
||||
|
||||
func paymentEndpointData(method *endpointv1.PaymentMethod) any {
|
||||
if method == nil {
|
||||
return nil
|
||||
}
|
||||
switch method.GetType() {
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
|
||||
type ledgerMethodData struct {
|
||||
LedgerAccountRef string `bson:"ledgerAccountRef"`
|
||||
ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty"`
|
||||
}
|
||||
var payload ledgerMethodData
|
||||
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
return srequest.LedgerEndpoint{
|
||||
LedgerAccountRef: strings.TrimSpace(payload.LedgerAccountRef),
|
||||
ContraLedgerAccountRef: strings.TrimSpace(payload.ContraLedgerAccountRef),
|
||||
}
|
||||
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
|
||||
type walletMethodData struct {
|
||||
WalletID string `bson:"walletId"`
|
||||
}
|
||||
var payload walletMethodData
|
||||
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
return srequest.WalletEndpoint{
|
||||
WalletID: strings.TrimSpace(payload.WalletID),
|
||||
}
|
||||
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
|
||||
type cryptoMethodData struct {
|
||||
Currency string `bson:"currency"`
|
||||
Address string `bson:"address"`
|
||||
Network string `bson:"network"`
|
||||
DestinationTag *string `bson:"destinationTag,omitempty"`
|
||||
}
|
||||
var payload cryptoMethodData
|
||||
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
endpoint := srequest.ExternalChainEndpoint{
|
||||
Asset: &srequest.Asset{
|
||||
Chain: parseChainNetwork(payload.Network),
|
||||
TokenSymbol: strings.ToUpper(strings.TrimSpace(payload.Currency)),
|
||||
},
|
||||
Address: strings.TrimSpace(payload.Address),
|
||||
}
|
||||
if memo := strings.TrimSpace(strPtr(payload.DestinationTag)); memo != "" {
|
||||
endpoint.Memo = memo
|
||||
}
|
||||
if endpoint.Asset.Chain == srequest.ChainNetworkUnspecified && endpoint.Asset.TokenSymbol == "" {
|
||||
endpoint.Asset = nil
|
||||
}
|
||||
return endpoint
|
||||
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
|
||||
type cardMethodData struct {
|
||||
Pan string `bson:"pan"`
|
||||
FirstName string `bson:"firstName"`
|
||||
LastName string `bson:"lastName"`
|
||||
ExpMonth string `bson:"expMonth"`
|
||||
ExpYear string `bson:"expYear"`
|
||||
Country string `bson:"country,omitempty"`
|
||||
}
|
||||
var payload cardMethodData
|
||||
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
return srequest.CardEndpoint{
|
||||
Pan: strings.TrimSpace(payload.Pan),
|
||||
FirstName: strings.TrimSpace(payload.FirstName),
|
||||
LastName: strings.TrimSpace(payload.LastName),
|
||||
ExpMonth: parseUint32(payload.ExpMonth),
|
||||
ExpYear: parseUint32(payload.ExpYear),
|
||||
Country: strings.TrimSpace(payload.Country),
|
||||
}
|
||||
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
|
||||
type cardTokenMethodData struct {
|
||||
Token string `bson:"token"`
|
||||
Last4 string `bson:"last4,omitempty"`
|
||||
}
|
||||
var payload cardTokenMethodData
|
||||
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
return srequest.CardTokenEndpoint{
|
||||
Token: strings.TrimSpace(payload.Token),
|
||||
MaskedPan: strings.TrimSpace(payload.Last4),
|
||||
}
|
||||
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
|
||||
type bankAccountMethodData struct {
|
||||
RecipientName string `bson:"recipientName"`
|
||||
Inn string `bson:"inn"`
|
||||
Kpp string `bson:"kpp"`
|
||||
BankName string `bson:"bankName"`
|
||||
Bik string `bson:"bik"`
|
||||
AccountNumber string `bson:"accountNumber"`
|
||||
CorrespondentAccount string `bson:"correspondentAccount"`
|
||||
}
|
||||
var payload bankAccountMethodData
|
||||
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
return srequest.BankAccountEndpoint{
|
||||
RecipientName: strings.TrimSpace(payload.RecipientName),
|
||||
Inn: strings.TrimSpace(payload.Inn),
|
||||
Kpp: strings.TrimSpace(payload.Kpp),
|
||||
BankName: strings.TrimSpace(payload.BankName),
|
||||
Bik: strings.TrimSpace(payload.Bik),
|
||||
AccountNumber: strings.TrimSpace(payload.AccountNumber),
|
||||
CorrespondentAccount: strings.TrimSpace(payload.CorrespondentAccount),
|
||||
}
|
||||
|
||||
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
|
||||
type ibanMethodData struct {
|
||||
IBAN string `bson:"iban"`
|
||||
AccountHolder string `bson:"accountHolder"`
|
||||
BIC *string `bson:"bic,omitempty"`
|
||||
BankName *string `bson:"bankName,omitempty"`
|
||||
}
|
||||
var payload ibanMethodData
|
||||
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
return srequest.IBANEndpoint{
|
||||
IBAN: strings.TrimSpace(payload.IBAN),
|
||||
AccountHolder: strings.TrimSpace(payload.AccountHolder),
|
||||
BIC: strings.TrimSpace(strPtr(payload.BIC)),
|
||||
BankName: strings.TrimSpace(strPtr(payload.BankName)),
|
||||
}
|
||||
|
||||
default:
|
||||
return toRawBSON(method.GetData())
|
||||
}
|
||||
}
|
||||
|
||||
func toRawBSON(raw []byte) map[string]any {
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
var data map[string]any
|
||||
if err := bson.Unmarshal(raw, &data); err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func parseChainNetwork(value string) srequest.ChainNetwork {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "ETHEREUM_MAINNET":
|
||||
return srequest.ChainNetworkEthereumMainnet
|
||||
case "ARBITRUM_ONE":
|
||||
return srequest.ChainNetworkArbitrumOne
|
||||
case "TRON_MAINNET":
|
||||
return srequest.ChainNetworkTronMainnet
|
||||
case "TRON_NILE":
|
||||
return srequest.ChainNetworkTronNile
|
||||
case "", "UNSPECIFIED":
|
||||
return srequest.ChainNetworkUnspecified
|
||||
default:
|
||||
return srequest.ChainNetwork(strings.ToLower(strings.TrimSpace(value)))
|
||||
}
|
||||
}
|
||||
|
||||
func parseUint32(value string) uint32 {
|
||||
clean := strings.TrimSpace(value)
|
||||
if clean == "" {
|
||||
return 0
|
||||
}
|
||||
parsed, err := strconv.ParseUint(clean, 10, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return uint32(parsed)
|
||||
}
|
||||
|
||||
func strPtr(v *string) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func firstFailure(operations []PaymentOperation) (string, string) {
|
||||
for _, op := range operations {
|
||||
if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
|
||||
@@ -326,13 +580,20 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOp
|
||||
}
|
||||
|
||||
func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
|
||||
operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs())
|
||||
amount := normalizeOperationMoney(toMoney(step.GetExecutedMoney()))
|
||||
convertedAmount := normalizeOperationMoney(toMoney(step.GetConvertedMoney()))
|
||||
op := PaymentOperation{
|
||||
StepRef: step.GetStepRef(),
|
||||
Code: step.GetStepCode(),
|
||||
State: enumJSONName(step.GetState().String()),
|
||||
Label: strings.TrimSpace(step.GetUserLabel()),
|
||||
StartedAt: timestampAsTime(step.GetStartedAt()),
|
||||
CompletedAt: timestampAsTime(step.GetCompletedAt()),
|
||||
StepRef: step.GetStepRef(),
|
||||
Code: step.GetStepCode(),
|
||||
State: enumJSONName(step.GetState().String()),
|
||||
Label: strings.TrimSpace(step.GetUserLabel()),
|
||||
Amount: amount,
|
||||
ConvertedAmount: convertedAmount,
|
||||
OperationRef: operationRef,
|
||||
Gateway: gateway,
|
||||
StartedAt: timestampAsTime(step.GetStartedAt()),
|
||||
CompletedAt: timestampAsTime(step.GetCompletedAt()),
|
||||
}
|
||||
failure := step.GetFailure()
|
||||
if failure == nil {
|
||||
@@ -346,6 +607,132 @@ func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
|
||||
return op
|
||||
}
|
||||
|
||||
func normalizeOperationMoney(value *paymenttypes.Money) *paymenttypes.Money {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(value.GetAmount())
|
||||
currency := strings.TrimSpace(value.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
externalRefKindOperation = "operation_ref"
|
||||
)
|
||||
|
||||
func operationRefAndGateway(stepCode string, refs []*orchestrationv2.ExternalReference) (string, mservice.Type) {
|
||||
var (
|
||||
operationRef string
|
||||
gateway mservice.Type
|
||||
)
|
||||
|
||||
for _, ref := range refs {
|
||||
if ref == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
kind := strings.ToLower(strings.TrimSpace(ref.GetKind()))
|
||||
value := strings.TrimSpace(ref.GetRef())
|
||||
candidateGateway := inferGatewayType(ref.GetGatewayInstanceId(), ref.GetRail(), stepCode)
|
||||
|
||||
if kind == externalRefKindOperation && operationRef == "" && value != "" {
|
||||
operationRef = value
|
||||
}
|
||||
if gateway == "" && candidateGateway != "" {
|
||||
gateway = candidateGateway
|
||||
}
|
||||
}
|
||||
if gateway == "" {
|
||||
gateway = inferGatewayType("", gatewayv1.Rail_RAIL_UNSPECIFIED, stepCode)
|
||||
}
|
||||
return operationRef, gateway
|
||||
}
|
||||
|
||||
func inferGatewayType(gatewayInstanceID string, rail gatewayv1.Rail, stepCode string) mservice.Type {
|
||||
if gateway := gatewayTypeFromInstanceID(gatewayInstanceID); gateway != "" {
|
||||
return gateway
|
||||
}
|
||||
if gateway := gatewayTypeFromRail(rail); gateway != "" {
|
||||
return gateway
|
||||
}
|
||||
return gatewayTypeFromStepCode(stepCode)
|
||||
}
|
||||
|
||||
func gatewayTypeFromInstanceID(raw string) mservice.Type {
|
||||
value := strings.ToLower(strings.TrimSpace(raw))
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch value {
|
||||
case mservice.ChainGateway, mservice.TronGateway, mservice.MntxGateway, mservice.PaymentGateway, mservice.TgSettle, mservice.Ledger:
|
||||
return value
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(value, "ledger"):
|
||||
return mservice.Ledger
|
||||
case strings.Contains(value, "tgsettle"):
|
||||
return mservice.TgSettle
|
||||
case strings.Contains(value, "payment_gateway"),
|
||||
strings.Contains(value, "settlement"),
|
||||
strings.Contains(value, "onramp"),
|
||||
strings.Contains(value, "offramp"):
|
||||
return mservice.PaymentGateway
|
||||
case strings.Contains(value, "mntx"), strings.Contains(value, "mcards"):
|
||||
return mservice.MntxGateway
|
||||
case strings.Contains(value, "tron"):
|
||||
return mservice.TronGateway
|
||||
case strings.Contains(value, "chain"), strings.Contains(value, "crypto"):
|
||||
return mservice.ChainGateway
|
||||
case strings.Contains(value, "card"):
|
||||
return mservice.MntxGateway
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func gatewayTypeFromRail(rail gatewayv1.Rail) mservice.Type {
|
||||
switch rail {
|
||||
case gatewayv1.Rail_RAIL_LEDGER:
|
||||
return mservice.Ledger
|
||||
case gatewayv1.Rail_RAIL_CARD:
|
||||
return mservice.MntxGateway
|
||||
case gatewayv1.Rail_RAIL_SETTLEMENT, gatewayv1.Rail_RAIL_ONRAMP, gatewayv1.Rail_RAIL_OFFRAMP:
|
||||
return mservice.PaymentGateway
|
||||
case gatewayv1.Rail_RAIL_CRYPTO:
|
||||
return mservice.ChainGateway
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func gatewayTypeFromStepCode(stepCode string) mservice.Type {
|
||||
code := strings.ToLower(strings.TrimSpace(stepCode))
|
||||
switch {
|
||||
case strings.Contains(code, "ledger"):
|
||||
return mservice.Ledger
|
||||
case strings.Contains(code, "card_payout"), strings.Contains(code, ".card."):
|
||||
return mservice.MntxGateway
|
||||
case strings.Contains(code, "provider_settlement"),
|
||||
strings.Contains(code, "settlement"),
|
||||
strings.Contains(code, "fx_convert"),
|
||||
strings.Contains(code, "onramp"),
|
||||
strings.Contains(code, "offramp"):
|
||||
return mservice.PaymentGateway
|
||||
case strings.Contains(code, "crypto"), strings.Contains(code, "chain"):
|
||||
return mservice.ChainGateway
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func isUserVisibleStep(visibility orchestrationv2.ReportVisibility) bool {
|
||||
switch visibility {
|
||||
case orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,
|
||||
|
||||
@@ -3,9 +3,14 @@ package sresponse
|
||||
import (
|
||||
"testing"
|
||||
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) {
|
||||
@@ -119,6 +124,141 @@ func TestToPaymentIgnoresHiddenFailures(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentMapsIntentComment(t *testing.T) {
|
||||
dto := toPayment(&orchestrationv2.Payment{
|
||||
PaymentRef: "pay-3",
|
||||
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
|
||||
IntentSnapshot: "ationv2.QuoteIntent{
|
||||
Comment: " invoice-7 ",
|
||||
},
|
||||
})
|
||||
if dto == nil {
|
||||
t.Fatal("expected non-nil payment dto")
|
||||
}
|
||||
if got, want := dto.Comment, "invoice-7"; got != want {
|
||||
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentMapsSourceAndDestination(t *testing.T) {
|
||||
sourceRaw, err := bson.Marshal(struct {
|
||||
WalletID string `bson:"walletId"`
|
||||
}{
|
||||
WalletID: "wallet-src-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal source method data: %v", err)
|
||||
}
|
||||
destinationRaw, err := bson.Marshal(struct {
|
||||
Currency string `bson:"currency"`
|
||||
Address string `bson:"address"`
|
||||
Network string `bson:"network"`
|
||||
DestinationTag *string `bson:"destinationTag,omitempty"`
|
||||
}{
|
||||
Currency: "USDT",
|
||||
Address: "TXabc",
|
||||
Network: "TRON_MAINNET",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal destination method data: %v", err)
|
||||
}
|
||||
|
||||
dto := toPayment(&orchestrationv2.Payment{
|
||||
PaymentRef: "pay-src-dst",
|
||||
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
|
||||
IntentSnapshot: "ationv2.QuoteIntent{
|
||||
Source: &endpointv1.PaymentEndpoint{
|
||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||
PaymentMethod: &endpointv1.PaymentMethod{
|
||||
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
|
||||
Data: sourceRaw,
|
||||
},
|
||||
},
|
||||
},
|
||||
Destination: &endpointv1.PaymentEndpoint{
|
||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||
PaymentMethod: &endpointv1.PaymentMethod{
|
||||
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS,
|
||||
Data: destinationRaw,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if dto == nil {
|
||||
t.Fatal("expected non-nil payment dto")
|
||||
}
|
||||
if dto.Source == nil {
|
||||
t.Fatal("expected source endpoint")
|
||||
}
|
||||
if got, want := dto.Source.Type, string(srequest.EndpointTypeWallet); got != want {
|
||||
t.Fatalf("source type mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
sourceEndpoint, ok := dto.Source.Data.(srequest.WalletEndpoint)
|
||||
if !ok {
|
||||
t.Fatalf("source endpoint payload type mismatch: got=%T", dto.Source.Data)
|
||||
}
|
||||
if got, want := sourceEndpoint.WalletID, "wallet-src-1"; got != want {
|
||||
t.Fatalf("source wallet id mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if dto.Destination == nil {
|
||||
t.Fatal("expected destination endpoint")
|
||||
}
|
||||
if got, want := dto.Destination.Type, string(srequest.EndpointTypeExternalChain); got != want {
|
||||
t.Fatalf("destination type mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
destinationEndpoint, ok := dto.Destination.Data.(srequest.ExternalChainEndpoint)
|
||||
if !ok {
|
||||
t.Fatalf("destination endpoint payload type mismatch: got=%T", dto.Destination.Data)
|
||||
}
|
||||
if got, want := destinationEndpoint.Address, "TXabc"; got != want {
|
||||
t.Fatalf("destination address mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if destinationEndpoint.Asset == nil {
|
||||
t.Fatal("expected destination asset")
|
||||
}
|
||||
if got, want := destinationEndpoint.Asset.TokenSymbol, "USDT"; got != want {
|
||||
t.Fatalf("destination token mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := destinationEndpoint.Asset.Chain, srequest.ChainNetworkTronMainnet; got != want {
|
||||
t.Fatalf("destination chain mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentMapsEndpointRefs(t *testing.T) {
|
||||
dto := toPayment(&orchestrationv2.Payment{
|
||||
PaymentRef: "pay-refs",
|
||||
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
|
||||
IntentSnapshot: "ationv2.QuoteIntent{
|
||||
Source: &endpointv1.PaymentEndpoint{
|
||||
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{
|
||||
PaymentMethodRef: "pm-123",
|
||||
},
|
||||
},
|
||||
Destination: &endpointv1.PaymentEndpoint{
|
||||
Source: &endpointv1.PaymentEndpoint_PayeeRef{
|
||||
PayeeRef: "payee-777",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if dto == nil {
|
||||
t.Fatal("expected non-nil payment dto")
|
||||
}
|
||||
if dto.Source == nil {
|
||||
t.Fatal("expected source endpoint")
|
||||
}
|
||||
if got, want := dto.Source.PaymentMethodRef, "pm-123"; got != want {
|
||||
t.Fatalf("source payment_method_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if dto.Destination == nil {
|
||||
t.Fatal("expected destination endpoint")
|
||||
}
|
||||
if got, want := dto.Destination.PayeeRef, "payee-777"; got != want {
|
||||
t.Fatalf("destination payee_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
|
||||
dto := toPaymentQuote("ationv2.PaymentQuote{
|
||||
QuoteRef: "quote-1",
|
||||
@@ -134,3 +274,152 @@ func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
|
||||
t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_MapsOperationRefAndGateway(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-1",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
Refs: []*orchestrationv2.ExternalReference{
|
||||
{
|
||||
Rail: gatewayv1.Rail_RAIL_CARD,
|
||||
GatewayInstanceId: "mcards",
|
||||
Kind: "operation_ref",
|
||||
Ref: "op-123",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if got, want := op.OperationRef, "op-123"; got != want {
|
||||
t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Gateway, "mntx_gateway"; got != want {
|
||||
t.Fatalf("gateway mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_InfersGatewayFromStepCode(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-2",
|
||||
StepCode: "edge.1_2.ledger.debit",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
})
|
||||
|
||||
if got := op.OperationRef; got != "" {
|
||||
t.Fatalf("expected empty operation_ref, got=%q", got)
|
||||
}
|
||||
if got, want := op.Gateway, "ledger"; got != want {
|
||||
t.Fatalf("gateway mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_DoesNotFallbackToCardPayoutRef(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-3",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
Refs: []*orchestrationv2.ExternalReference{
|
||||
{
|
||||
Rail: gatewayv1.Rail_RAIL_CARD,
|
||||
GatewayInstanceId: "mcards",
|
||||
Kind: "card_payout_ref",
|
||||
Ref: "payout-123",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if got := op.OperationRef; got != "" {
|
||||
t.Fatalf("expected empty operation_ref, got=%q", got)
|
||||
}
|
||||
if got, want := op.Gateway, "mntx_gateway"; got != want {
|
||||
t.Fatalf("gateway mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_MapsAmount(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-4",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
})
|
||||
|
||||
if got := op.Amount; got != nil {
|
||||
t.Fatalf("expected nil amount without executed_money, got=%+v", got)
|
||||
}
|
||||
if got := op.ConvertedAmount; got != nil {
|
||||
t.Fatalf("expected no converted_amount for non-fx operation, got=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_PrefersExecutedMoney(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-4b",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
ExecutedMoney: &moneyv1.Money{Amount: "99.95", Currency: "EUR"},
|
||||
})
|
||||
|
||||
if op.Amount == nil {
|
||||
t.Fatal("expected amount to be mapped")
|
||||
}
|
||||
if got, want := op.Amount.Amount, "99.95"; got != want {
|
||||
t.Fatalf("amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Amount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := op.ConvertedAmount; got != nil {
|
||||
t.Fatalf("expected no converted_amount for non-fx operation, got=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_MapsFxTwoAmounts(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-5",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
ConvertedMoney: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
})
|
||||
|
||||
if got := op.Amount; got != nil {
|
||||
t.Fatalf("expected nil base amount without executed_money, got=%+v", got)
|
||||
}
|
||||
if op.ConvertedAmount == nil {
|
||||
t.Fatal("expected fx converted amount to be mapped")
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToPaymentOperation_FxWithExecutedMoney_StillProvidesTwoAmounts(t *testing.T) {
|
||||
op := toPaymentOperation(&orchestrationv2.StepExecution{
|
||||
StepRef: "step-6",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
ExecutedMoney: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
|
||||
ConvertedMoney: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
})
|
||||
|
||||
if op.Amount == nil {
|
||||
t.Fatal("expected fx base amount to be mapped")
|
||||
}
|
||||
if got, want := op.Amount.Amount, "109.50"; got != want {
|
||||
t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.Amount.Currency, "USDT"; got != want {
|
||||
t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if op.ConvertedAmount == nil {
|
||||
t.Fatal("expected fx quote amount to be mapped")
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Amount, "100.00"; got != want {
|
||||
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := op.ConvertedAmount.Currency, "EUR"; got != want {
|
||||
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func Account2ClaimsForClient(a *model.Account, expiration int, clientID string)
|
||||
paramNameName: t.Name,
|
||||
paramNameLocale: t.Locale,
|
||||
paramNameClientID: t.ClientID,
|
||||
paramNameExpiration: int64(t.Expiration.Unix()),
|
||||
paramNameExpiration: t.Expiration.Unix(),
|
||||
paramNamePending: t.Pending,
|
||||
}
|
||||
}
|
||||
|
||||
11
api/edge/bff/interface/services/callbacks/callbacks.go
Normal file
11
api/edge/bff/interface/services/callbacks/callbacks.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package callbacks
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/callbacksimp"
|
||||
)
|
||||
|
||||
func Create(a api.API) (mservice.MicroService, error) {
|
||||
return callbacksimp.CreateAPI(a)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/interface/services/account"
|
||||
"github.com/tech/sendico/server/interface/services/callbacks"
|
||||
"github.com/tech/sendico/server/interface/services/invitation"
|
||||
"github.com/tech/sendico/server/interface/services/ledger"
|
||||
"github.com/tech/sendico/server/interface/services/logo"
|
||||
@@ -91,6 +92,7 @@ func (a *APIImp) installServices() error {
|
||||
srvf = append(srvf, wallet.Create)
|
||||
srvf = append(srvf, ledger.Create)
|
||||
srvf = append(srvf, recipient.Create)
|
||||
srvf = append(srvf, callbacks.Create)
|
||||
srvf = append(srvf, paymethod.Create)
|
||||
srvf = append(srvf, payment.Create)
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ const (
|
||||
var (
|
||||
ledgerDiscoveryServiceNames = []string{
|
||||
"LEDGER",
|
||||
string(mservice.Ledger),
|
||||
mservice.Ledger,
|
||||
}
|
||||
paymentOrchestratorDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_ORCHESTRATOR",
|
||||
string(mservice.PaymentOrchestrator),
|
||||
mservice.PaymentOrchestrator,
|
||||
}
|
||||
paymentQuotationDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_QUOTATION",
|
||||
@@ -41,7 +41,7 @@ var (
|
||||
paymentMethodsDiscoveryServiceNames = []string{
|
||||
"PAYMENTS_METHODS",
|
||||
"PAYMENT_METHODS",
|
||||
string(mservice.PaymentMethods),
|
||||
mservice.PaymentMethods,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -339,13 +339,13 @@ func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork
|
||||
func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Invoke uri is empty")
|
||||
return discoveryEndpoint{}, fmt.Errorf("invoke uri is empty")
|
||||
}
|
||||
|
||||
// Without a scheme we expect a plain host:port target.
|
||||
if !strings.Contains(raw, "://") {
|
||||
if _, _, err := net.SplitHostPort(raw); err != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Invoke uri must include host:port: %w", err)
|
||||
return discoveryEndpoint{}, fmt.Errorf("invoke uri must include host:port: %w", err)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: raw,
|
||||
@@ -363,7 +363,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
|
||||
case "grpc":
|
||||
address := strings.TrimSpace(parsed.Host)
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Grpc invoke uri must include host:port: %w", splitErr)
|
||||
return discoveryEndpoint{}, fmt.Errorf("grpc invoke uri must include host:port: %w", splitErr)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: address,
|
||||
@@ -373,7 +373,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
|
||||
case "grpcs":
|
||||
address := strings.TrimSpace(parsed.Host)
|
||||
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
|
||||
return discoveryEndpoint{}, fmt.Errorf("Grpcs invoke uri must include host:port: %w", splitErr)
|
||||
return discoveryEndpoint{}, fmt.Errorf("grpcs invoke uri must include host:port: %w", splitErr)
|
||||
}
|
||||
return discoveryEndpoint{
|
||||
address: address,
|
||||
@@ -388,7 +388,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
|
||||
raw: raw,
|
||||
}, nil
|
||||
default:
|
||||
return discoveryEndpoint{}, fmt.Errorf("Unsupported invoke uri scheme: %s", parsed.Scheme)
|
||||
return discoveryEndpoint{}, fmt.Errorf("unsupported invoke uri scheme: %s", parsed.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ func hasGrantType(grants []string, target string) bool {
|
||||
|
||||
func (pr *PublicRouter) validateClientIPPolicy(r *http.Request, clientID string, client *model.Client) http.HandlerFunc {
|
||||
if client == nil {
|
||||
pr.logger.Info("Client not found, rejecting authorization", zap.String("client_id", clientID))
|
||||
return response.Unauthorized(pr.logger, pr.service, "client not found")
|
||||
}
|
||||
clientIP := ipguard.ClientIP(r)
|
||||
|
||||
@@ -42,9 +42,7 @@ func PrepareRefreshToken(
|
||||
}
|
||||
|
||||
token := &model.RefreshToken{
|
||||
AccountBoundBase: model.AccountBoundBase{
|
||||
AccountRef: account.GetID(),
|
||||
},
|
||||
AccountRef: account.GetID(),
|
||||
ClientRefreshToken: model.ClientRefreshToken{
|
||||
SessionIdentifier: *session,
|
||||
RefreshToken: refreshToken,
|
||||
|
||||
@@ -43,6 +43,7 @@ func (d *DispatcherImpl) dispatchMessage(ctx context.Context, conn *websocket.Co
|
||||
}
|
||||
|
||||
func (d *DispatcherImpl) handle(w http.ResponseWriter, r *http.Request) {
|
||||
//nolint:contextcheck // websocket.Handler callback signature does not carry request context.
|
||||
websocket.Handler(func(conn *websocket.Conn) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(d.timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -71,6 +71,7 @@ func GetOptionalParam[T any](logger mlogger.Logger, r *http.Request, key string,
|
||||
vals := r.URL.Query()
|
||||
s := vals.Get(key)
|
||||
if s == "" {
|
||||
//nolint:nilnil // Missing optional query parameter is represented as (nil, nil).
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package mutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestGetOptionalBoolParam(t *testing.T) {
|
||||
logger := mlogger.Logger(zap.NewNop())
|
||||
logger := zap.NewNop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -47,7 +47,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := GetOptionalBoolParam(logger, req, "param")
|
||||
@@ -69,7 +69,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetOptionalInt64Param(t *testing.T) {
|
||||
logger := mlogger.Logger(zap.NewNop())
|
||||
logger := zap.NewNop()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -111,7 +111,7 @@ func TestGetOptionalInt64Param(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := GetOptionalInt64Param(logger, req, "param")
|
||||
|
||||
@@ -114,7 +114,7 @@ func (a *AccountAPI) deleteAll(r *http.Request, account *model.Account, token *s
|
||||
a.logger.Warn("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
return struct{}{}, nil
|
||||
}); err != nil {
|
||||
a.logger.Warn("Failed to execute delete transaction", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
|
||||
@@ -120,11 +120,11 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
|
||||
var user model.Account
|
||||
err = a.db.Get(ctx, accountRef, &user)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
a.logger.Info("User not found for password reset", zap.String("account_ref", accountRef.Hex()))
|
||||
a.logger.Info("User not found for password reset", mzap.ObjRef("account_ref", accountRef))
|
||||
return response.NotFound(a.logger, a.Name(), "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to get user for password reset", zap.Error(err), zap.String("account_ref", accountRef.Hex()))
|
||||
a.logger.Warn("Failed to get user for password reset", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
|
||||
}
|
||||
|
||||
if t.AccountRef != accountRef {
|
||||
a.logger.Warn("Token account reference does not match request account reference", zap.String("token_account_ref", t.AccountRef.Hex()), zap.String("request_account_ref", accountRef.Hex()))
|
||||
a.logger.Warn("Token account reference does not match request account reference", mzap.ObjRef("token_account_ref", t.AccountRef), mzap.ObjRef("request_account_ref", accountRef))
|
||||
return response.DataConflict(a.logger, a.Name(), "Token does not match account")
|
||||
}
|
||||
|
||||
@@ -192,5 +192,5 @@ func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *mod
|
||||
// Don't fail the transaction if token revocation fails, but log it
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return struct{}{}, nil
|
||||
}
|
||||
|
||||
@@ -133,12 +133,7 @@ func TestPasswordValidationLogic(t *testing.T) {
|
||||
for _, password := range invalidPasswords {
|
||||
t.Run(password, func(t *testing.T) {
|
||||
// Test that invalid passwords fail at least one requirement
|
||||
isValid := true
|
||||
|
||||
// Check length
|
||||
if len(password) < 8 {
|
||||
isValid = false
|
||||
}
|
||||
isValid := len(password) >= 8
|
||||
|
||||
// Check for digit
|
||||
hasDigit := false
|
||||
|
||||
@@ -276,7 +276,7 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef bs
|
||||
|
||||
for resource, granted := range required {
|
||||
if !granted {
|
||||
a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", string(resource)))
|
||||
a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", resource))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,9 +338,6 @@ func (a *AccountAPI) openOrgLedgerAccount(ctx context.Context, org *model.Organi
|
||||
return merrors.Internal("chain gateway default asset is not configured")
|
||||
}
|
||||
|
||||
// TODO: remove hardcode
|
||||
currency := "RUB"
|
||||
|
||||
var describable *describablev1.Describable
|
||||
name := strings.TrimSpace(sr.LedgerWallet.Name)
|
||||
var description *string
|
||||
@@ -357,26 +354,47 @@ func (a *AccountAPI) openOrgLedgerAccount(ctx context.Context, org *model.Organi
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := a.ledgerClient.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||
OrganizationRef: org.ID.Hex(),
|
||||
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
|
||||
Currency: currency,
|
||||
Status: ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE,
|
||||
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING,
|
||||
Metadata: map[string]string{
|
||||
"source": "signup",
|
||||
"login": sr.Account.Login,
|
||||
},
|
||||
Describable: describable,
|
||||
})
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to create ledger account for organization", zap.Error(err), mzap.StorableRef(org))
|
||||
return err
|
||||
}
|
||||
if resp == nil || resp.GetAccount() == nil || strings.TrimSpace(resp.GetAccount().GetLedgerAccountRef()) == "" {
|
||||
return merrors.Internal("ledger returned empty account reference")
|
||||
currencies := []string{"RUB", "USDT"}
|
||||
if chainTokenCurrency := strings.ToUpper(strings.TrimSpace(a.chainAsset.GetTokenSymbol())); chainTokenCurrency != "" {
|
||||
currencies = append(currencies, chainTokenCurrency)
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(currencies))
|
||||
for _, currency := range currencies {
|
||||
currency = strings.ToUpper(strings.TrimSpace(currency))
|
||||
if currency == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[currency]; exists {
|
||||
continue
|
||||
}
|
||||
seen[currency] = struct{}{}
|
||||
|
||||
resp, err := a.ledgerClient.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||
OrganizationRef: org.ID.Hex(),
|
||||
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
|
||||
Currency: currency,
|
||||
Status: ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE,
|
||||
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING,
|
||||
Metadata: map[string]string{
|
||||
"source": "signup",
|
||||
"login": sr.Account.Login,
|
||||
},
|
||||
Describable: describable,
|
||||
})
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to create ledger account for organization", zap.Error(err), mzap.StorableRef(org), zap.String("currency", currency))
|
||||
return err
|
||||
}
|
||||
if resp == nil || resp.GetAccount() == nil || strings.TrimSpace(resp.GetAccount().GetLedgerAccountRef()) == "" {
|
||||
return merrors.Internal("ledger returned empty account reference")
|
||||
}
|
||||
|
||||
a.logger.Info("Ledger account created for organization",
|
||||
mzap.StorableRef(org),
|
||||
zap.String("currency", currency),
|
||||
zap.String("ledger_account_ref", resp.GetAccount().GetLedgerAccountRef()))
|
||||
}
|
||||
|
||||
a.logger.Info("Ledger account created for organization", mzap.StorableRef(org), zap.String("ledger_account_ref", resp.GetAccount().GetLedgerAccountRef()))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create HTTP request
|
||||
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Parse the request body
|
||||
@@ -162,7 +162,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
|
||||
t.Run("InvalidJSONRequest", func(t *testing.T) {
|
||||
invalidJSON := `{"account": {"login": "test@example.com", "password": "invalid json structure`
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON))
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
var parsedRequest srequest.Signup
|
||||
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
)
|
||||
|
||||
type stubLedgerAccountClient struct {
|
||||
createReq *ledgerv1.CreateAccountRequest
|
||||
createReqs []*ledgerv1.CreateAccountRequest
|
||||
createResp *ledgerv1.CreateAccountResponse
|
||||
createErr error
|
||||
}
|
||||
|
||||
func (s *stubLedgerAccountClient) CreateAccount(_ context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||
s.createReq = req
|
||||
s.createReqs = append(s.createReqs, req)
|
||||
return s.createResp, s.createErr
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (s *stubLedgerAccountClient) Close() error {
|
||||
}
|
||||
|
||||
func TestOpenOrgLedgerAccount(t *testing.T) {
|
||||
t.Run("creates operating ledger account", func(t *testing.T) {
|
||||
t.Run("creates operating ledger accounts for RUB and USDT", func(t *testing.T) {
|
||||
desc := " Main org ledger account "
|
||||
sr := &srequest.Signup{
|
||||
Account: model.AccountData{
|
||||
@@ -65,22 +65,26 @@ func TestOpenOrgLedgerAccount(t *testing.T) {
|
||||
|
||||
err := api.openOrgLedgerAccount(context.Background(), org, sr)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, ledgerStub.createReq) {
|
||||
assert.Equal(t, org.ID.Hex(), ledgerStub.createReq.GetOrganizationRef())
|
||||
assert.Equal(t, "RUB", ledgerStub.createReq.GetCurrency())
|
||||
assert.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, ledgerStub.createReq.GetAccountType())
|
||||
assert.Equal(t, ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, ledgerStub.createReq.GetStatus())
|
||||
assert.Equal(t, ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, ledgerStub.createReq.GetRole())
|
||||
assert.Equal(t, map[string]string{
|
||||
"source": "signup",
|
||||
"login": "owner@example.com",
|
||||
}, ledgerStub.createReq.GetMetadata())
|
||||
if assert.NotNil(t, ledgerStub.createReq.GetDescribable()) {
|
||||
assert.Equal(t, "Primary Ledger", ledgerStub.createReq.GetDescribable().GetName())
|
||||
if assert.NotNil(t, ledgerStub.createReq.GetDescribable().Description) {
|
||||
assert.Equal(t, "Main org ledger account", ledgerStub.createReq.GetDescribable().GetDescription())
|
||||
if assert.Len(t, ledgerStub.createReqs, 2) {
|
||||
currencies := make([]string, 0, len(ledgerStub.createReqs))
|
||||
for _, req := range ledgerStub.createReqs {
|
||||
currencies = append(currencies, req.GetCurrency())
|
||||
assert.Equal(t, org.ID.Hex(), req.GetOrganizationRef())
|
||||
assert.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, req.GetAccountType())
|
||||
assert.Equal(t, ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, req.GetStatus())
|
||||
assert.Equal(t, ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, req.GetRole())
|
||||
assert.Equal(t, map[string]string{
|
||||
"source": "signup",
|
||||
"login": "owner@example.com",
|
||||
}, req.GetMetadata())
|
||||
if assert.NotNil(t, req.GetDescribable()) {
|
||||
assert.Equal(t, "Primary Ledger", req.GetDescribable().GetName())
|
||||
if assert.NotNil(t, req.GetDescribable().Description) {
|
||||
assert.Equal(t, "Main org ledger account", req.GetDescribable().GetDescription())
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"RUB", "USDT"}, currencies)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
47
api/edge/bff/internal/server/callbacksimp/create.go
Normal file
47
api/edge/bff/internal/server/callbacksimp/create.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
|
||||
organizationRef, err := a.Oph.GetRef(r)
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to parse organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
|
||||
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
|
||||
}
|
||||
|
||||
var callback model.Callback
|
||||
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
|
||||
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
|
||||
return response.BadPayload(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
mutation, err := a.normalizeAndPrepare(r.Context(), &callback, organizationRef, "", true)
|
||||
if err != nil {
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
|
||||
if err := a.DB.Create(ctx, *account.GetID(), organizationRef, &callback); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.applySigningSecretMutation(ctx, *account.GetID(), *callback.GetID(), mutation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return struct{}{}, nil
|
||||
}); err != nil {
|
||||
a.Logger.Warn("Failed to create callback transaction", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return a.callbackResponse(&callback, accessToken, mutation.Generated, true)
|
||||
}
|
||||
285
api/edge/bff/internal/server/callbacksimp/rotate.go
Normal file
285
api/edge/bff/internal/server/callbacksimp/rotate.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type signingSecretMutation struct {
|
||||
SetSecretRef string
|
||||
Clear bool
|
||||
Generated string
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) rotateSecret(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
|
||||
callbackRef, err := a.Cph.GetRef(r)
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to parse callback reference", zap.Error(err), mutil.PLog(a.Cph, r))
|
||||
return response.BadReference(a.Logger, a.Name(), a.Cph.Name(), a.Cph.GetID(r), err)
|
||||
}
|
||||
|
||||
var callback model.Callback
|
||||
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &callback); err != nil {
|
||||
a.Logger.Warn("Failed to fetch callback for secret rotation", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if callback.RetryPolicy.SigningMode != model.CallbackSigningModeHMACSHA256 {
|
||||
return response.BadRequest(a.Logger, a.Name(), "invalid_signing_mode", "rotate-secret is available only for hmac_sha256 callbacks")
|
||||
}
|
||||
|
||||
secretRef, generatedSecret, err := a.secrets.Provision(r.Context(), callback.OrganizationRef, callbackRef)
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to rotate callback signing secret", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
if err := a.db.SetSigningSecretRef(r.Context(), *account.GetID(), callbackRef, secretRef); err != nil {
|
||||
a.Logger.Warn("Failed to persist rotated callback signing secret reference", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return a.callbackResponse(&callback, accessToken, generatedSecret, false)
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) normalizeAndPrepare(
|
||||
ctx context.Context,
|
||||
callback *model.Callback,
|
||||
organizationRef bson.ObjectID,
|
||||
existingSecretRef string,
|
||||
allowSecretGeneration bool,
|
||||
) (signingSecretMutation, error) {
|
||||
if callback == nil {
|
||||
return signingSecretMutation{}, merrors.InvalidArgument("callback payload is required")
|
||||
}
|
||||
if organizationRef.IsZero() {
|
||||
return signingSecretMutation{}, merrors.InvalidArgument("organization reference is required", "organizationRef")
|
||||
}
|
||||
|
||||
callback.Name = strings.TrimSpace(callback.Name)
|
||||
callback.Description = trimDescription(callback.Description)
|
||||
|
||||
callback.URL = strings.TrimSpace(callback.URL)
|
||||
if callback.URL == "" {
|
||||
return signingSecretMutation{}, merrors.InvalidArgument("url is required", "url")
|
||||
}
|
||||
if err := validateCallbackURL(callback.URL); err != nil {
|
||||
return signingSecretMutation{}, err
|
||||
}
|
||||
if callback.Name == "" {
|
||||
callback.Name = callback.URL
|
||||
}
|
||||
|
||||
status, err := normalizeStatus(callback.Status, a.config.DefaultStatus)
|
||||
if err != nil {
|
||||
return signingSecretMutation{}, err
|
||||
}
|
||||
callback.Status = status
|
||||
callback.EventTypes = normalizeEventTypes(callback.EventTypes, a.config.DefaultEventTypes)
|
||||
|
||||
callback.RetryPolicy.Backoff.MinDelayMS = defaultInt(callback.RetryPolicy.Backoff.MinDelayMS, defaultRetryMinDelayMS)
|
||||
callback.RetryPolicy.Backoff.MaxDelayMS = defaultInt(callback.RetryPolicy.Backoff.MaxDelayMS, defaultRetryMaxDelayMS)
|
||||
if callback.RetryPolicy.Backoff.MaxDelayMS < callback.RetryPolicy.Backoff.MinDelayMS {
|
||||
callback.RetryPolicy.Backoff.MaxDelayMS = callback.RetryPolicy.Backoff.MinDelayMS
|
||||
}
|
||||
callback.RetryPolicy.MaxAttempts = defaultInt(callback.RetryPolicy.MaxAttempts, defaultRetryMaxAttempts)
|
||||
callback.RetryPolicy.RequestTimeoutMS = defaultInt(callback.RetryPolicy.RequestTimeoutMS, defaultRetryRequestTimeoutMS)
|
||||
callback.RetryPolicy.Headers = normalizeHeaders(callback.RetryPolicy.Headers)
|
||||
|
||||
mode, err := normalizeSigningMode(callback.RetryPolicy.SigningMode)
|
||||
if err != nil {
|
||||
return signingSecretMutation{}, err
|
||||
}
|
||||
callback.RetryPolicy.SigningMode = mode
|
||||
|
||||
existingSecretRef = strings.TrimSpace(existingSecretRef)
|
||||
switch callback.RetryPolicy.SigningMode {
|
||||
case model.CallbackSigningModeNone:
|
||||
return signingSecretMutation{Clear: existingSecretRef != ""}, nil
|
||||
case model.CallbackSigningModeHMACSHA256:
|
||||
if existingSecretRef != "" {
|
||||
return signingSecretMutation{SetSecretRef: existingSecretRef}, nil
|
||||
}
|
||||
if !allowSecretGeneration {
|
||||
return signingSecretMutation{}, merrors.InvalidArgument("signing secret is required for hmac_sha256 callbacks", "retryPolicy.signingMode")
|
||||
}
|
||||
if callback.GetID().IsZero() {
|
||||
callback.SetID(bson.NewObjectID())
|
||||
}
|
||||
secretRef, generatedSecret, err := a.secrets.Provision(ctx, organizationRef, *callback.GetID())
|
||||
if err != nil {
|
||||
return signingSecretMutation{}, err
|
||||
}
|
||||
return signingSecretMutation{SetSecretRef: secretRef, Generated: generatedSecret}, nil
|
||||
default:
|
||||
return signingSecretMutation{}, merrors.InvalidArgument("unsupported signing mode", "retryPolicy.signingMode")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) applySigningSecretMutation(
|
||||
ctx context.Context,
|
||||
accountRef,
|
||||
callbackRef bson.ObjectID,
|
||||
mutation signingSecretMutation,
|
||||
) error {
|
||||
if callbackRef.IsZero() {
|
||||
return merrors.InvalidArgument("callback reference is required", "callbackRef")
|
||||
}
|
||||
if strings.TrimSpace(mutation.SetSecretRef) != "" {
|
||||
return a.db.SetSigningSecretRef(ctx, accountRef, callbackRef, mutation.SetSecretRef)
|
||||
}
|
||||
if mutation.Clear {
|
||||
err := a.db.ClearSigningSecretRef(ctx, accountRef, callbackRef)
|
||||
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) callbackResponse(
|
||||
callback *model.Callback,
|
||||
accessToken *sresponse.TokenData,
|
||||
generatedSecret string,
|
||||
created bool,
|
||||
) http.HandlerFunc {
|
||||
if callback == nil || accessToken == nil {
|
||||
return response.Internal(a.Logger, a.Name(), merrors.Internal("failed to build callback response"))
|
||||
}
|
||||
|
||||
return sresponse.Callback(a.Logger, callback, accessToken, generatedSecret, created)
|
||||
}
|
||||
|
||||
func normalizeStatus(raw, fallback model.CallbackStatus) (model.CallbackStatus, error) {
|
||||
candidate := strings.ToLower(strings.TrimSpace(string(raw)))
|
||||
if candidate == "" {
|
||||
candidate = strings.ToLower(strings.TrimSpace(string(fallback)))
|
||||
}
|
||||
|
||||
switch candidate {
|
||||
case "", "active", "enabled":
|
||||
return model.CallbackStatusActive, nil
|
||||
case "disabled", "inactive":
|
||||
return model.CallbackStatusDisabled, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument("unsupported callback status", "status")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSigningMode(raw model.CallbackSigningMode) (model.CallbackSigningMode, error) {
|
||||
mode := strings.ToLower(strings.TrimSpace(string(raw)))
|
||||
switch mode {
|
||||
case "", "none":
|
||||
return model.CallbackSigningModeNone, nil
|
||||
case "hmac_sha256", "hmac-sha256", "hmac":
|
||||
return model.CallbackSigningModeHMACSHA256, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument("unsupported callback signing mode", "retryPolicy.signingMode")
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeEventTypes(eventTypes []string, defaults []string) []string {
|
||||
if len(eventTypes) == 0 {
|
||||
return normalizeEventTypes(defaults, nil)
|
||||
}
|
||||
seen := make(map[string]struct{}, len(eventTypes))
|
||||
out := make([]string, 0, len(eventTypes))
|
||||
for _, eventType := range eventTypes {
|
||||
value := strings.TrimSpace(eventType)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[value]; exists {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
out = append(out, value)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
if len(defaults) > 0 {
|
||||
return normalizeEventTypes(defaults, nil)
|
||||
}
|
||||
return []string{model.PaymentStatusUpdatedType}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeHeaders(headers map[string]string) map[string]string {
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(headers))
|
||||
for key, value := range headers {
|
||||
k := strings.TrimSpace(key)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
out[k] = strings.TrimSpace(value)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeCallbackMutable(dst, src *model.Callback) {
|
||||
dst.OrganizationRef = src.OrganizationRef
|
||||
dst.Describable = src.Describable
|
||||
dst.Status = src.Status
|
||||
dst.URL = src.URL
|
||||
dst.EventTypes = append([]string(nil), src.EventTypes...)
|
||||
dst.RetryPolicy = model.CallbackRetryPolicy{
|
||||
Backoff: model.CallbackBackoff{
|
||||
MinDelayMS: src.RetryPolicy.Backoff.MinDelayMS,
|
||||
MaxDelayMS: src.RetryPolicy.Backoff.MaxDelayMS,
|
||||
},
|
||||
SigningMode: src.RetryPolicy.SigningMode,
|
||||
Headers: normalizeHeaders(src.RetryPolicy.Headers),
|
||||
MaxAttempts: src.RetryPolicy.MaxAttempts,
|
||||
RequestTimeoutMS: src.RetryPolicy.RequestTimeoutMS,
|
||||
}
|
||||
}
|
||||
|
||||
func defaultInt(value, fallback int) int {
|
||||
if value > 0 {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func trimDescription(in *string) *string {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
value := strings.TrimSpace(*in)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func validateCallbackURL(raw string) error {
|
||||
parsed, err := url.ParseRequestURI(raw)
|
||||
if err != nil {
|
||||
return merrors.InvalidArgument("url is invalid", "url")
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
|
||||
case "https", "http":
|
||||
default:
|
||||
return merrors.InvalidArgument("url scheme must be http or https", "url")
|
||||
}
|
||||
if strings.TrimSpace(parsed.Host) == "" {
|
||||
return merrors.InvalidArgument("url host is required", "url")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
179
api/edge/bff/internal/server/callbacksimp/secrets.go
Normal file
179
api/edge/bff/internal/server/callbacksimp/secrets.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/pkg/vault/kv"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type signingSecretManager interface {
|
||||
Provision(ctx context.Context, organizationRef, callbackRef bson.ObjectID) (secretRef string, generatedSecret string, err error)
|
||||
}
|
||||
|
||||
type vaultSigningSecretManager struct {
|
||||
logger mlogger.Logger
|
||||
store kv.Client
|
||||
pathPrefix string
|
||||
field string
|
||||
secretLength int
|
||||
}
|
||||
|
||||
const (
|
||||
metricsResultSuccess = "success"
|
||||
metricsResultError = "error"
|
||||
)
|
||||
|
||||
var (
|
||||
signingSecretMetricsOnce sync.Once
|
||||
signingSecretStatus *prometheus.CounterVec
|
||||
signingSecretLatency *prometheus.HistogramVec
|
||||
)
|
||||
|
||||
func ensureSigningSecretMetrics() {
|
||||
signingSecretMetricsOnce.Do(func() {
|
||||
signingSecretStatus = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "sendico",
|
||||
Subsystem: "bff_callbacks",
|
||||
Name: "signing_secret_provision_total",
|
||||
Help: "Total callback signing secret provisioning attempts.",
|
||||
}, []string{"result"})
|
||||
signingSecretLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Namespace: "sendico",
|
||||
Subsystem: "bff_callbacks",
|
||||
Name: "signing_secret_provision_duration_seconds",
|
||||
Help: "Duration of callback signing secret provisioning attempts.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, []string{"result"})
|
||||
})
|
||||
}
|
||||
|
||||
func newSigningSecretManager(logger mlogger.Logger, cfg callbacksConfig) (signingSecretManager, error) {
|
||||
if err := cfg.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
|
||||
manager := &vaultSigningSecretManager{
|
||||
logger: logger.Named("callbacks_secrets"),
|
||||
pathPrefix: strings.Trim(strings.TrimSpace(cfg.SecretPathPrefix), "/"),
|
||||
field: strings.TrimSpace(cfg.SecretField),
|
||||
secretLength: cfg.SecretLengthBytes,
|
||||
}
|
||||
if manager.pathPrefix == "" {
|
||||
manager.pathPrefix = defaultSigningSecretPathPrefix
|
||||
}
|
||||
if manager.field == "" {
|
||||
manager.field = defaultSigningSecretField
|
||||
}
|
||||
|
||||
if isVaultConfigEmpty(cfg.Vault) {
|
||||
manager.logger.Warn("Callbacks Vault config is not set; hmac signing secret generation is disabled")
|
||||
ensureSigningSecretMetrics()
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
store, err := kv.New(kv.Options{
|
||||
Logger: manager.logger,
|
||||
Config: kv.Config{
|
||||
Address: strings.TrimSpace(cfg.Vault.Address),
|
||||
TokenEnv: strings.TrimSpace(cfg.Vault.TokenEnv),
|
||||
TokenFileEnv: strings.TrimSpace(cfg.Vault.TokenFileEnv),
|
||||
TokenFile: strings.TrimSpace(cfg.Vault.TokenFile),
|
||||
Namespace: strings.TrimSpace(cfg.Vault.Namespace),
|
||||
MountPath: strings.TrimSpace(cfg.Vault.MountPath),
|
||||
},
|
||||
Component: "bff callbacks signing secret manager",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager.store = store
|
||||
ensureSigningSecretMetrics()
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (m *vaultSigningSecretManager) Provision(
|
||||
ctx context.Context,
|
||||
organizationRef,
|
||||
callbackRef bson.ObjectID,
|
||||
) (string, string, error) {
|
||||
start := time.Now()
|
||||
result := metricsResultSuccess
|
||||
defer func() {
|
||||
signingSecretStatus.WithLabelValues(result).Inc()
|
||||
signingSecretLatency.WithLabelValues(result).Observe(time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
if organizationRef.IsZero() {
|
||||
result = metricsResultError
|
||||
return "", "", merrors.InvalidArgument("organization reference is required", "organizationRef")
|
||||
}
|
||||
if callbackRef.IsZero() {
|
||||
result = metricsResultError
|
||||
return "", "", merrors.InvalidArgument("callback reference is required", "callbackRef")
|
||||
}
|
||||
if m.store == nil {
|
||||
result = metricsResultError
|
||||
return "", "", merrors.InvalidArgument("callbacks vault config is required to generate signing secrets", "api.callbacks.vault")
|
||||
}
|
||||
|
||||
secret, err := generateSigningSecret(m.secretLength)
|
||||
if err != nil {
|
||||
result = metricsResultError
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
secretPath := path.Join(m.pathPrefix, organizationRef.Hex(), callbackRef.Hex())
|
||||
payload := map[string]interface{}{
|
||||
m.field: secret,
|
||||
"organization_ref": organizationRef.Hex(),
|
||||
"callback_ref": callbackRef.Hex(),
|
||||
"updated_at": time.Now().UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
if err := m.store.Put(ctx, secretPath, payload); err != nil {
|
||||
result = metricsResultError
|
||||
m.logger.Warn("Failed to store callback signing secret", zap.String("path", secretPath), zap.Error(err))
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
secretRef := "vault:" + secretPath + "#" + m.field
|
||||
m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), mzap.ObjRef("callback_ref", callbackRef))
|
||||
|
||||
return secretRef, secret, nil
|
||||
}
|
||||
|
||||
func isVaultConfigEmpty(cfg VaultConfig) bool {
|
||||
return strings.TrimSpace(cfg.Address) == "" &&
|
||||
strings.TrimSpace(cfg.TokenEnv) == "" &&
|
||||
strings.TrimSpace(cfg.TokenFileEnv) == "" &&
|
||||
strings.TrimSpace(cfg.TokenFile) == "" &&
|
||||
strings.TrimSpace(cfg.MountPath) == "" &&
|
||||
strings.TrimSpace(cfg.Namespace) == ""
|
||||
}
|
||||
|
||||
func generateSigningSecret(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", merrors.InvalidArgument("secret length must be greater than zero", "secret_length")
|
||||
}
|
||||
raw := make([]byte, length)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return "", merrors.Internal("failed to generate signing secret: " + err.Error())
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(raw), nil
|
||||
}
|
||||
142
api/edge/bff/internal/server/callbacksimp/service.go
Normal file
142
api/edge/bff/internal/server/callbacksimp/service.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/db/callbacks"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
eapi "github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/internal/server/papitemplate"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CallbacksAPI struct {
|
||||
papitemplate.ProtectedAPI[model.Callback]
|
||||
db callbacks.DB
|
||||
tf transaction.Factory
|
||||
secrets signingSecretManager
|
||||
config callbacksConfig
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) Name() mservice.Type {
|
||||
return mservice.Callbacks
|
||||
}
|
||||
|
||||
func (a *CallbacksAPI) Finish(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateAPI(apiCtx eapi.API) (*CallbacksAPI, error) {
|
||||
dbFactory := func() (papitemplate.ProtectedDB[model.Callback], error) {
|
||||
return apiCtx.DBFactory().NewCallbacksDB()
|
||||
}
|
||||
|
||||
res := &CallbacksAPI{
|
||||
config: newCallbacksConfig(apiCtx.Config().Callbacks),
|
||||
tf: apiCtx.DBFactory().TransactionFactory(),
|
||||
}
|
||||
|
||||
p, err := papitemplate.CreateAPI(apiCtx, dbFactory, mservice.Organizations, mservice.Callbacks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.ProtectedAPI = *p.
|
||||
WithNoCreateNotification().
|
||||
WithNoUpdateNotification().
|
||||
WithNoDeleteNotification().
|
||||
WithCreateHandler(res.create).
|
||||
WithUpdateHandler(res.update).
|
||||
Build()
|
||||
|
||||
if res.db, err = apiCtx.DBFactory().NewCallbacksDB(); err != nil {
|
||||
res.Logger.Warn("Failed to create callbacks database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.secrets, err = newSigningSecretManager(res.Logger, res.config); err != nil {
|
||||
res.Logger.Warn("Failed to initialize callbacks signing secret manager", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiCtx.Register().AccountHandler(res.Name(), res.Cph.AddRef("/rotate-secret"), api.Post, res.rotateSecret)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
const (
|
||||
defaultCallbackStatus = model.CallbackStatusActive
|
||||
defaultRetryMaxAttempts = 8
|
||||
defaultRetryMinDelayMS = 1000
|
||||
defaultRetryMaxDelayMS = 300000
|
||||
defaultRetryRequestTimeoutMS = 10000
|
||||
defaultSigningSecretLengthBytes = 32
|
||||
defaultSigningSecretField = "value"
|
||||
defaultSigningSecretPathPrefix = "sendico/callbacks"
|
||||
)
|
||||
|
||||
type callbacksConfig struct {
|
||||
DefaultEventTypes []string
|
||||
DefaultStatus model.CallbackStatus
|
||||
SecretPathPrefix string
|
||||
SecretField string
|
||||
SecretLengthBytes int
|
||||
Vault VaultConfig
|
||||
}
|
||||
|
||||
type VaultConfig struct {
|
||||
Address string
|
||||
TokenEnv string
|
||||
TokenFileEnv string
|
||||
TokenFile string
|
||||
Namespace string
|
||||
MountPath string
|
||||
}
|
||||
|
||||
func newCallbacksConfig(source *eapi.CallbacksConfig) callbacksConfig {
|
||||
cfg := callbacksConfig{
|
||||
DefaultEventTypes: []string{model.PaymentStatusUpdatedType},
|
||||
DefaultStatus: defaultCallbackStatus,
|
||||
SecretPathPrefix: defaultSigningSecretPathPrefix,
|
||||
SecretField: defaultSigningSecretField,
|
||||
SecretLengthBytes: defaultSigningSecretLengthBytes,
|
||||
}
|
||||
if source == nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
if source.SecretPathPrefix != "" {
|
||||
cfg.SecretPathPrefix = source.SecretPathPrefix
|
||||
}
|
||||
if source.SecretField != "" {
|
||||
cfg.SecretField = source.SecretField
|
||||
}
|
||||
if source.SecretLengthBytes > 0 {
|
||||
cfg.SecretLengthBytes = source.SecretLengthBytes
|
||||
}
|
||||
if len(source.DefaultEventTypes) > 0 {
|
||||
cfg.DefaultEventTypes = source.DefaultEventTypes
|
||||
}
|
||||
if source.DefaultStatus != "" {
|
||||
cfg.DefaultStatus = model.CallbackStatus(source.DefaultStatus)
|
||||
}
|
||||
cfg.Vault = VaultConfig{
|
||||
Address: source.Vault.Address,
|
||||
TokenEnv: source.Vault.TokenEnv,
|
||||
TokenFileEnv: source.Vault.TokenFileEnv,
|
||||
TokenFile: source.Vault.TokenFile,
|
||||
Namespace: source.Vault.Namespace,
|
||||
MountPath: source.Vault.MountPath,
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c callbacksConfig) validate() error {
|
||||
if c.SecretLengthBytes <= 0 {
|
||||
return merrors.InvalidArgument("callbacks signing secret length must be greater than zero", "api.callbacks.secret_length_bytes")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
59
api/edge/bff/internal/server/callbacksimp/update.go
Normal file
59
api/edge/bff/internal/server/callbacksimp/update.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package callbacksimp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
|
||||
var input model.Callback
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
|
||||
return response.BadPayload(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
callbackRef := *input.GetID()
|
||||
if callbackRef.IsZero() {
|
||||
return response.Auto(a.Logger, a.Name(), merrors.InvalidArgument("callback ref is required", "id"))
|
||||
}
|
||||
|
||||
var existing model.Callback
|
||||
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &existing); err != nil {
|
||||
a.Logger.Warn("Failed to fetch callback before update", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
existingSecretRef, err := a.db.GetSigningSecretRef(r.Context(), *account.GetID(), callbackRef)
|
||||
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||
a.Logger.Warn("Failed to fetch callback signing secret metadata", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
mergeCallbackMutable(&existing, &input)
|
||||
mutation, err := a.normalizeAndPrepare(r.Context(), &existing, existing.OrganizationRef, existingSecretRef, true)
|
||||
if err != nil {
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
|
||||
if err := a.DB.Update(ctx, *account.GetID(), &existing); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.applySigningSecretMutation(ctx, *account.GetID(), callbackRef, mutation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return struct{}{}, nil
|
||||
}); err != nil {
|
||||
a.Logger.Warn("Failed to update callback transaction", zap.Error(err))
|
||||
return response.Auto(a.Logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return a.callbackResponse(&existing, accessToken, mutation.Generated, false)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/domainprovider"
|
||||
@@ -34,7 +35,10 @@ func (storage *LocalStorage) Delete(ctx context.Context, objID string) error {
|
||||
default:
|
||||
}
|
||||
|
||||
filePath := filepath.Join(storage.storageDir, objID)
|
||||
filePath, err := storage.resolvePath(objID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
storage.logger.Debug("File not found", zap.String("obj_ref", objID))
|
||||
@@ -54,7 +58,11 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
|
||||
default:
|
||||
}
|
||||
|
||||
filePath := filepath.Join(storage.storageDir, objID)
|
||||
filePath, err := storage.resolvePath(objID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
//nolint:gosec // File path is resolved and constrained to storage root.
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID))
|
||||
@@ -78,7 +86,9 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// Context was cancelled, clean up the partial file
|
||||
os.Remove(filePath)
|
||||
if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) {
|
||||
storage.logger.Warn("Failed to remove partially written file", zap.Error(removeErr), zap.String("obj_ref", objID))
|
||||
}
|
||||
return "", ctx.Err()
|
||||
}
|
||||
|
||||
@@ -93,7 +103,10 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
|
||||
default:
|
||||
}
|
||||
|
||||
filePath := filepath.Join(storage.storageDir, objRef)
|
||||
filePath, err := storage.resolvePath(objRef)
|
||||
if err != nil {
|
||||
return response.Internal(storage.logger, storage.service, err)
|
||||
}
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef))
|
||||
return response.Internal(storage.logger, storage.service, err)
|
||||
@@ -117,7 +130,7 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
|
||||
func ensureDir(dirName string) error {
|
||||
info, err := os.Stat(dirName)
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(dirName, 0o755)
|
||||
return os.MkdirAll(dirName, 0o750)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -128,6 +141,24 @@ func ensureDir(dirName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *LocalStorage) resolvePath(objID string) (string, error) {
|
||||
objID = strings.TrimSpace(objID)
|
||||
if objID == "" {
|
||||
return "", merrors.InvalidArgument("obj_ref is required", "obj_ref")
|
||||
}
|
||||
|
||||
filePath := filepath.Join(storage.storageDir, objID)
|
||||
relPath, err := filepath.Rel(storage.storageDir, filePath)
|
||||
if err != nil {
|
||||
return "", merrors.InternalWrap(err, "failed to resolve local file path")
|
||||
}
|
||||
if relPath == "." || strings.HasPrefix(relPath, "..") {
|
||||
return "", merrors.InvalidArgument("obj_ref is invalid", "obj_ref")
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) {
|
||||
dir := filepath.Join(cfg.RootPath, directory)
|
||||
if err := ensureDir(dir); err != nil {
|
||||
|
||||
@@ -55,7 +55,7 @@ func setupTestStorage(t *testing.T) (*LocalStorage, string, func()) {
|
||||
|
||||
// Return cleanup function
|
||||
cleanup := func() {
|
||||
os.RemoveAll(tempDir)
|
||||
require.NoError(t, os.RemoveAll(tempDir))
|
||||
}
|
||||
|
||||
return storage, tempDir, cleanup
|
||||
@@ -81,7 +81,7 @@ func setupBenchmarkStorage(b *testing.B) (*LocalStorage, string, func()) {
|
||||
|
||||
// Return cleanup function
|
||||
cleanup := func() {
|
||||
os.RemoveAll(tempDir)
|
||||
require.NoError(b, os.RemoveAll(tempDir))
|
||||
}
|
||||
|
||||
return storage, tempDir, cleanup
|
||||
@@ -138,6 +138,7 @@ func TestLocalStorage_Save(t *testing.T) {
|
||||
|
||||
// Verify file was actually saved
|
||||
filePath := filepath.Join(tempDir, tt.objID)
|
||||
//nolint:gosec // Test-controlled path inside temporary directory.
|
||||
content, err := os.ReadFile(filePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.content, string(content))
|
||||
@@ -186,7 +187,7 @@ func TestLocalStorage_Delete(t *testing.T) {
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
@@ -232,7 +233,7 @@ func TestLocalStorage_Delete_ContextCancellation(t *testing.T) {
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a context that's already cancelled
|
||||
@@ -256,7 +257,7 @@ func TestLocalStorage_Get(t *testing.T) {
|
||||
// Create a test file
|
||||
testContent := "test file content"
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte(testContent), 0o644)
|
||||
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
@@ -285,7 +286,7 @@ func TestLocalStorage_Get(t *testing.T) {
|
||||
handler := storage.Get(ctx, tt.objID)
|
||||
|
||||
// Create test request
|
||||
req := httptest.NewRequest("GET", "/files/"+tt.objID, nil)
|
||||
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/"+tt.objID, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
@@ -304,7 +305,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a context that's already cancelled
|
||||
@@ -314,7 +315,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
|
||||
handler := storage.Get(ctx, "test.txt")
|
||||
|
||||
// Create test request
|
||||
req := httptest.NewRequest("GET", "/files/test.txt", nil)
|
||||
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
@@ -328,14 +329,14 @@ func TestLocalStorage_Get_RequestContextCancellation(t *testing.T) {
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
handler := storage.Get(ctx, "test.txt")
|
||||
|
||||
// Create test request with cancelled context
|
||||
req := httptest.NewRequest("GET", "/files/test.txt", nil)
|
||||
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
|
||||
reqCtx, cancel := context.WithCancel(req.Context())
|
||||
req = req.WithContext(reqCtx)
|
||||
cancel() // Cancel the request context
|
||||
@@ -352,7 +353,9 @@ func TestCreateLocalFileStorage(t *testing.T) {
|
||||
// Create temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "storage_test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer func() {
|
||||
require.NoError(t, os.RemoveAll(tempDir))
|
||||
}()
|
||||
|
||||
logger := zap.NewNop()
|
||||
cfg := config.LocalFSSConfig{
|
||||
@@ -372,10 +375,12 @@ func TestCreateLocalFileStorage_InvalidPath(t *testing.T) {
|
||||
// Build a deterministic failure case: the target path already exists as a file.
|
||||
tempDir, err := os.MkdirTemp("", "storage_invalid_path_test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
defer func() {
|
||||
require.NoError(t, os.RemoveAll(tempDir))
|
||||
}()
|
||||
|
||||
fileAtTargetPath := filepath.Join(tempDir, "test")
|
||||
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o644)
|
||||
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
logger := zap.NewNop()
|
||||
@@ -426,7 +431,7 @@ func TestLocalStorage_ConcurrentOperations(t *testing.T) {
|
||||
// Create files to delete
|
||||
for i := 0; i < 5; i++ {
|
||||
filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i))
|
||||
err := os.WriteFile(filePath, []byte("content"), 0o644)
|
||||
err := os.WriteFile(filePath, []byte("content"), 0o600)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -536,7 +541,7 @@ func BenchmarkLocalStorage_Delete(b *testing.B) {
|
||||
// Pre-create files for deletion
|
||||
for i := 0; i < b.N; i++ {
|
||||
filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i))
|
||||
err := os.WriteFile(filePath, []byte("content"), 0o644)
|
||||
err := os.WriteFile(filePath, []byte("content"), 0o600)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
@@ -88,7 +89,7 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
|
||||
Describable: describable,
|
||||
})
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to create ledger account", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||
a.logger.Warn("Failed to create ledger account", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return response.Auto(a.logger, mservice.Ledger, err)
|
||||
}
|
||||
|
||||
@@ -96,7 +97,9 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
|
||||
}
|
||||
|
||||
func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) {
|
||||
defer r.Body.Close()
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
|
||||
payload := srequest.CreateLedgerAccount{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
|
||||
@@ -47,7 +47,7 @@ func (a *LedgerAPI) listAccounts(r *http.Request, account *model.Account, token
|
||||
|
||||
resp, err := a.client.ListAccounts(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to list ledger accounts", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||
a.logger.Warn("Failed to list ledger accounts", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return response.Auto(a.logger, mservice.Ledger, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ func (a *ProtectedAPI[T]) archive(r *http.Request, account *model.Account, acces
|
||||
|
||||
ctx := r.Context()
|
||||
_, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
|
||||
return nil, a.DB.SetArchived(r.Context(), *account.GetID(), organizationRef, objectRef, *archived, *cascade)
|
||||
return nil, a.DB.SetArchived(ctx, *account.GetID(), organizationRef, objectRef, *archived, *cascade)
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Warn("Failed to change archive property", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r),
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -23,43 +24,90 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const (
|
||||
documentsServiceName = "BILLING_DOCUMENTS"
|
||||
documentsOperationGet = discovery.OperationDocumentsGet
|
||||
documentsDialTimeout = 5 * time.Second
|
||||
documentsCallTimeout = 10 * time.Second
|
||||
gatewayCallTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
||||
var allowedOperationGatewayServices = map[mservice.Type]struct{}{
|
||||
mservice.ChainGateway: {},
|
||||
mservice.TronGateway: {},
|
||||
mservice.MntxGateway: {},
|
||||
mservice.PaymentGateway: {},
|
||||
mservice.TgSettle: {},
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
||||
orgRef, denied := a.authorizeDocumentDownload(r, account)
|
||||
if denied != nil {
|
||||
return denied
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
gatewayService := normalizeGatewayService(query.Get("gateway_service"))
|
||||
if gatewayService == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "gateway_service is required")
|
||||
}
|
||||
if _, ok := allowedOperationGatewayServices[gatewayService]; !ok {
|
||||
return response.BadRequest(a.logger, a.Name(), "invalid_parameter", "unsupported gateway_service")
|
||||
}
|
||||
|
||||
operationRef := strings.TrimSpace(query.Get("operation_ref"))
|
||||
if operationRef == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "operation_ref is required")
|
||||
}
|
||||
|
||||
service, gateway, h := a.resolveOperationDocumentDeps(r.Context(), gatewayService)
|
||||
if h != nil {
|
||||
return h
|
||||
}
|
||||
|
||||
op, err := a.fetchGatewayOperation(r.Context(), gateway.InvokeURI, operationRef)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
|
||||
return documentErrorResponse(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
req := operationDocumentRequest(orgRef.Hex(), gatewayService, operationRef, op)
|
||||
|
||||
docResp, err := a.fetchOperationDocument(r.Context(), service.InvokeURI, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
|
||||
return documentErrorResponse(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return operationDocumentResponse(a.logger, a.Name(), docResp, fmt.Sprintf("operation_%s.pdf", sanitizeFilenameComponent(operationRef)))
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) authorizeDocumentDownload(r *http.Request, account *model.Account) (bson.ObjectID, http.HandlerFunc) {
|
||||
orgRef, err := a.oph.GetRef(r)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to parse organization reference for document request", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
||||
return bson.NilObjectID, response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, bson.NilObjectID, model.ActionRead)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
return bson.NilObjectID, response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
if !allowed {
|
||||
a.logger.Debug("Access denied when downloading act", mutil.PLog(a.oph, r))
|
||||
return response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
|
||||
a.logger.Debug("Access denied when downloading document", mutil.PLog(a.oph, r))
|
||||
return bson.NilObjectID, response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
|
||||
}
|
||||
|
||||
paymentRef := strings.TrimSpace(r.URL.Query().Get("payment_ref"))
|
||||
if paymentRef == "" {
|
||||
paymentRef = strings.TrimSpace(r.URL.Query().Get("paymentRef"))
|
||||
}
|
||||
if paymentRef == "" {
|
||||
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "payment_ref is required")
|
||||
}
|
||||
return orgRef, nil
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) resolveOperationDocumentDeps(ctx context.Context, gatewayService mservice.Type) (*discovery.ServiceSummary, *discovery.GatewaySummary, http.HandlerFunc) {
|
||||
if a.discovery == nil {
|
||||
return response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "discovery client is not configured")
|
||||
return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "discovery client is not configured")
|
||||
}
|
||||
|
||||
lookupCtx, cancel := context.WithTimeout(ctx, discoveryLookupTimeout)
|
||||
@@ -68,27 +116,35 @@ func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *
|
||||
lookupResp, err := a.discovery.Lookup(lookupCtx)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to lookup discovery registry", zap.Error(err))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
return nil, nil, response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
service := findDocumentsService(lookupResp.Services)
|
||||
if service == nil {
|
||||
return response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "billing documents service unavailable")
|
||||
return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "billing documents service unavailable")
|
||||
}
|
||||
|
||||
docResp, err := a.fetchActDocument(ctx, service.InvokeURI, paymentRef)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to fetch act document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return documentErrorResponse(a.logger, a.Name(), err)
|
||||
gateway := findGatewayForService(lookupResp.Gateways, gatewayService)
|
||||
if gateway == nil {
|
||||
return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "gateway service unavailable")
|
||||
}
|
||||
if len(docResp.GetContent()) == 0 {
|
||||
return response.Error(a.logger, a.Name(), http.StatusInternalServerError, "empty_document", "document service returned empty payload")
|
||||
|
||||
return service, gateway, nil
|
||||
}
|
||||
|
||||
func operationDocumentResponse(logger mlogger.Logger, source mservice.Type, docResp *documentsv1.GetDocumentResponse, fallbackFilename string) http.HandlerFunc {
|
||||
if docResp == nil || len(docResp.GetContent()) == 0 {
|
||||
return response.Error(logger, source, http.StatusInternalServerError, "empty_document", "document service returned empty payload")
|
||||
}
|
||||
|
||||
filename := strings.TrimSpace(docResp.GetFilename())
|
||||
if filename == "" {
|
||||
filename = fmt.Sprintf("act_%s.pdf", paymentRef)
|
||||
filename = strings.TrimSpace(fallbackFilename)
|
||||
}
|
||||
if filename == "" {
|
||||
filename = "document.pdf"
|
||||
}
|
||||
|
||||
mimeType := strings.TrimSpace(docResp.GetMimeType())
|
||||
if mimeType == "" {
|
||||
mimeType = "application/pdf"
|
||||
@@ -98,31 +154,241 @@ func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *
|
||||
w.Header().Set("Content-Type", mimeType)
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, writeErr := w.Write(docResp.GetContent()); writeErr != nil {
|
||||
a.logger.Warn("Failed to write document response", zap.Error(writeErr))
|
||||
//nolint:gosec // Binary payload is served as attachment with explicit content type.
|
||||
if _, err := w.Write(docResp.GetContent()); err != nil {
|
||||
logger.Warn("Failed to write document response", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) fetchActDocument(ctx context.Context, invokeURI, paymentRef string) (*documentsv1.GetDocumentResponse, error) {
|
||||
dialCtx, cancel := context.WithTimeout(ctx, documentsDialTimeout)
|
||||
defer cancel()
|
||||
func normalizeGatewayService(raw string) mservice.Type {
|
||||
value := strings.ToLower(strings.TrimSpace(raw))
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(dialCtx, invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
switch value {
|
||||
case mservice.ChainGateway:
|
||||
return mservice.ChainGateway
|
||||
case mservice.TronGateway:
|
||||
return mservice.TronGateway
|
||||
case mservice.MntxGateway:
|
||||
return mservice.MntxGateway
|
||||
case mservice.PaymentGateway:
|
||||
return mservice.PaymentGateway
|
||||
case mservice.TgSettle:
|
||||
return mservice.TgSettle
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeFilenameComponent(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(len(trimmed))
|
||||
|
||||
for _, r := range trimmed {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
b.WriteRune(r)
|
||||
case r >= 'A' && r <= 'Z':
|
||||
b.WriteRune(r)
|
||||
case r >= '0' && r <= '9':
|
||||
b.WriteRune(r)
|
||||
case r == '-', r == '_':
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteRune('_')
|
||||
}
|
||||
}
|
||||
|
||||
clean := strings.Trim(b.String(), "_")
|
||||
if clean == "" {
|
||||
return "operation"
|
||||
}
|
||||
|
||||
return clean
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) fetchOperationDocument(ctx context.Context, invokeURI string, req *documentsv1.GetOperationDocumentRequest) (*documentsv1.GetDocumentResponse, error) {
|
||||
conn, err := grpc.NewClient(invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, merrors.InternalWrap(err, "dial billing documents")
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() {
|
||||
if closeErr := conn.Close(); closeErr != nil {
|
||||
a.logger.Warn("Failed to close billing documents connection", zap.Error(closeErr))
|
||||
}
|
||||
}()
|
||||
|
||||
client := documentsv1.NewDocumentServiceClient(conn)
|
||||
|
||||
callCtx, callCancel := context.WithTimeout(ctx, documentsCallTimeout)
|
||||
defer callCancel()
|
||||
|
||||
return client.GetDocument(callCtx, &documentsv1.GetDocumentRequest{
|
||||
PaymentRef: paymentRef,
|
||||
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
|
||||
})
|
||||
return client.GetOperationDocument(callCtx, req)
|
||||
}
|
||||
|
||||
func (a *PaymentAPI) fetchGatewayOperation(ctx context.Context, invokeURI, operationRef string) (*connectorv1.Operation, error) {
|
||||
conn, err := grpc.NewClient(invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, merrors.InternalWrap(err, "dial gateway connector")
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := conn.Close(); closeErr != nil {
|
||||
a.logger.Warn("Failed to close gateway connector connection", zap.Error(closeErr))
|
||||
}
|
||||
}()
|
||||
|
||||
client := connectorv1.NewConnectorServiceClient(conn)
|
||||
|
||||
callCtx, callCancel := context.WithTimeout(ctx, gatewayCallTimeout)
|
||||
defer callCancel()
|
||||
|
||||
resp, err := client.GetOperation(callCtx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(operationRef)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
op := resp.GetOperation()
|
||||
if op == nil {
|
||||
return nil, merrors.NoData("gateway returned empty operation payload")
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService mservice.Type) *discovery.GatewaySummary {
|
||||
candidates := make([]discovery.GatewaySummary, 0, len(gateways))
|
||||
for _, gw := range gateways {
|
||||
if !gw.Healthy || strings.TrimSpace(gw.InvokeURI) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
rail := discovery.NormalizeRail(gw.Rail)
|
||||
network := strings.ToLower(strings.TrimSpace(gw.Network))
|
||||
switch gatewayService {
|
||||
case mservice.MntxGateway:
|
||||
if rail == discovery.NormalizeRail(discovery.RailCardPayout) {
|
||||
candidates = append(candidates, gw)
|
||||
}
|
||||
case mservice.PaymentGateway, mservice.TgSettle:
|
||||
if rail == discovery.NormalizeRail(discovery.RailProviderSettlement) {
|
||||
candidates = append(candidates, gw)
|
||||
}
|
||||
case mservice.TronGateway:
|
||||
if rail == discovery.NormalizeRail(discovery.RailCrypto) && strings.Contains(network, "tron") {
|
||||
candidates = append(candidates, gw)
|
||||
}
|
||||
case mservice.ChainGateway:
|
||||
if rail == discovery.NormalizeRail(discovery.RailCrypto) && !strings.Contains(network, "tron") {
|
||||
candidates = append(candidates, gw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 0 && gatewayService == mservice.ChainGateway {
|
||||
for _, gw := range gateways {
|
||||
if gw.Healthy && strings.TrimSpace(gw.InvokeURI) != "" && discovery.NormalizeRail(gw.Rail) == discovery.NormalizeRail(discovery.RailCrypto) {
|
||||
candidates = append(candidates, gw)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
best := candidates[0]
|
||||
for _, candidate := range candidates[1:] {
|
||||
if candidate.RoutingPriority > best.RoutingPriority {
|
||||
best = candidate
|
||||
}
|
||||
}
|
||||
|
||||
return &best
|
||||
}
|
||||
|
||||
func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest {
|
||||
req := &documentsv1.GetOperationDocumentRequest{
|
||||
OrganizationRef: strings.TrimSpace(organizationRef),
|
||||
GatewayService: gatewayService,
|
||||
OperationRef: firstNonEmpty(strings.TrimSpace(op.GetOperationRef()), strings.TrimSpace(requestedOperationRef)),
|
||||
OperationCode: strings.TrimSpace(op.GetType().String()),
|
||||
OperationLabel: operationLabel(op.GetType()),
|
||||
OperationState: strings.TrimSpace(op.GetStatus().String()),
|
||||
Amount: strings.TrimSpace(op.GetMoney().GetAmount()),
|
||||
Currency: strings.TrimSpace(op.GetMoney().GetCurrency()),
|
||||
}
|
||||
|
||||
if ts := op.GetCreatedAt(); ts != nil {
|
||||
req.StartedAtUnixMs = ts.AsTime().UnixMilli()
|
||||
}
|
||||
if ts := op.GetUpdatedAt(); ts != nil {
|
||||
req.CompletedAtUnixMs = ts.AsTime().UnixMilli()
|
||||
}
|
||||
|
||||
req.PaymentRef = operationParamValue(op.GetParams(), "payment_ref", "parent_payment_ref", "paymentRef", "parentPaymentRef")
|
||||
req.FailureCode = firstNonEmpty(
|
||||
operationParamValue(op.GetParams(), "failure_code", "provider_code", "error_code"),
|
||||
failureCodeFromStatus(op.GetStatus()),
|
||||
)
|
||||
req.FailureReason = operationParamValue(op.GetParams(), "failure_reason", "provider_message", "error", "message")
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func operationLabel(opType connectorv1.OperationType) string {
|
||||
switch opType {
|
||||
case connectorv1.OperationType_CREDIT:
|
||||
return "Credit"
|
||||
case connectorv1.OperationType_DEBIT:
|
||||
return "Debit"
|
||||
case connectorv1.OperationType_TRANSFER:
|
||||
return "Transfer"
|
||||
case connectorv1.OperationType_PAYOUT:
|
||||
return "Payout"
|
||||
case connectorv1.OperationType_FEE_ESTIMATE:
|
||||
return "Fee Estimate"
|
||||
case connectorv1.OperationType_FX:
|
||||
return "FX"
|
||||
case connectorv1.OperationType_GAS_TOPUP:
|
||||
return "Gas Top Up"
|
||||
default:
|
||||
return strings.TrimSpace(opType.String())
|
||||
}
|
||||
}
|
||||
|
||||
func failureCodeFromStatus(status connectorv1.OperationStatus) string {
|
||||
switch status {
|
||||
case connectorv1.OperationStatus_OPERATION_FAILED, connectorv1.OperationStatus_OPERATION_CANCELLED:
|
||||
return strings.TrimSpace(status.String())
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func operationParamValue(params *structpb.Struct, keys ...string) string {
|
||||
if params == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
values := params.AsMap()
|
||||
for _, key := range keys {
|
||||
raw, ok := values[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if text := strings.TrimSpace(fmt.Sprint(raw)); text != "" && text != "<nil>" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func findDocumentsService(services []discovery.ServiceSummary) *discovery.ServiceSummary {
|
||||
|
||||
@@ -103,6 +103,7 @@ func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error)
|
||||
}
|
||||
|
||||
if cursor == "" && !hasLimit {
|
||||
//nolint:nilnil // Absent pagination params mean no page request should be sent downstream.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -189,6 +190,7 @@ func firstNonEmpty(values ...string) string {
|
||||
func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
//nolint:nilnil // Empty timestamp filter is represented as (nil, nil).
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, trimmed)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
@@ -59,25 +60,36 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e
|
||||
SettlementMode: resolvedSettlementMode,
|
||||
FeeTreatment: resolvedFeeTreatment,
|
||||
SettlementCurrency: settlementCurrency,
|
||||
FxSide: mapFXSide(intent),
|
||||
}
|
||||
if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" {
|
||||
quoteIntent.Comment = comment
|
||||
Fx: mapFXIntent(intent),
|
||||
Comment: strings.TrimSpace(intent.Comment),
|
||||
}
|
||||
return quoteIntent, nil
|
||||
}
|
||||
|
||||
func mapFXSide(intent *srequest.PaymentIntent) fxv1.Side {
|
||||
if intent == nil || intent.FX == nil {
|
||||
return fxv1.Side_SIDE_UNSPECIFIED
|
||||
func mapFXIntent(intent *srequest.PaymentIntent) *sharedv1.FXIntent {
|
||||
if intent == nil || intent.FX == nil || intent.FX.Pair == nil {
|
||||
return nil
|
||||
}
|
||||
side := fxv1.Side_SIDE_UNSPECIFIED
|
||||
switch strings.TrimSpace(string(intent.FX.Side)) {
|
||||
case string(srequest.FXSideBuyBaseSellQuote):
|
||||
return fxv1.Side_BUY_BASE_SELL_QUOTE
|
||||
side = fxv1.Side_BUY_BASE_SELL_QUOTE
|
||||
case string(srequest.FXSideSellBaseBuyQuote):
|
||||
return fxv1.Side_SELL_BASE_BUY_QUOTE
|
||||
default:
|
||||
return fxv1.Side_SIDE_UNSPECIFIED
|
||||
side = fxv1.Side_SELL_BASE_BUY_QUOTE
|
||||
}
|
||||
if side == fxv1.Side_SIDE_UNSPECIFIED {
|
||||
side = fxv1.Side_SELL_BASE_BUY_QUOTE
|
||||
}
|
||||
return &sharedv1.FXIntent{
|
||||
Pair: &fxv1.CurrencyPair{
|
||||
Base: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Base)),
|
||||
Quote: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Quote)),
|
||||
},
|
||||
Side: side,
|
||||
Firm: intent.FX.Firm,
|
||||
TtlMs: intent.FX.TTLms,
|
||||
PreferredProvider: strings.TrimSpace(intent.FX.PreferredProvider),
|
||||
MaxAgeMs: intent.FX.MaxAgeMs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,8 +202,14 @@ func TestMapQuoteIntent_DerivesSettlementCurrencyFromFX(t *testing.T) {
|
||||
if got.GetSettlementCurrency() != "RUB" {
|
||||
t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency())
|
||||
}
|
||||
if got.GetFxSide() != fxv1.Side_SELL_BASE_BUY_QUOTE {
|
||||
t.Fatalf("unexpected fx_side: got=%s", got.GetFxSide().String())
|
||||
if got.GetFx() == nil || got.GetFx().GetPair() == nil {
|
||||
t.Fatalf("expected fx intent")
|
||||
}
|
||||
if got.GetFx().GetSide() != fxv1.Side_SELL_BASE_BUY_QUOTE {
|
||||
t.Fatalf("unexpected fx side: got=%s", got.GetFx().GetSide().String())
|
||||
}
|
||||
if got.GetFx().GetPair().GetBase() != "USDT" || got.GetFx().GetPair().GetQuote() != "RUB" {
|
||||
t.Fatalf("unexpected fx pair: got=%s/%s", got.GetFx().GetPair().GetBase(), got.GetFx().GetPair().GetQuote())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,8 +252,11 @@ func TestMapQuoteIntent_PropagatesFXSideBuyBaseSellQuote(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got.GetFxSide() != fxv1.Side_BUY_BASE_SELL_QUOTE {
|
||||
t.Fatalf("unexpected fx_side: got=%s", got.GetFxSide().String())
|
||||
if got.GetFx() == nil || got.GetFx().GetPair() == nil {
|
||||
t.Fatalf("expected fx intent")
|
||||
}
|
||||
if got.GetFx().GetSide() != fxv1.Side_BUY_BASE_SELL_QUOTE {
|
||||
t.Fatalf("unexpected fx side: got=%s", got.GetFx().GetSide().String())
|
||||
}
|
||||
if got.GetSettlementCurrency() != "RUB" {
|
||||
t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency())
|
||||
|
||||
@@ -89,7 +89,7 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
||||
req := &orchestrationv2.ExecutePaymentRequest{
|
||||
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
|
||||
QuotationRef: quotationRef,
|
||||
ClientPaymentRef: metadataValue(payload.Metadata, "client_payment_ref"),
|
||||
ClientPaymentRef: strings.TrimSpace(payload.ClientPaymentRef),
|
||||
}
|
||||
|
||||
resp, err := a.execution.ExecutePayment(ctx, req)
|
||||
@@ -102,7 +102,9 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
||||
}
|
||||
|
||||
func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
|
||||
defer r.Body.Close()
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
|
||||
payload := &srequest.InitiatePayment{}
|
||||
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||
@@ -110,6 +112,7 @@ func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
|
||||
}
|
||||
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
|
||||
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
|
||||
payload.ClientPaymentRef = strings.TrimSpace(payload.ClientPaymentRef)
|
||||
|
||||
if err := payload.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestInitiateByQuote_DoesNotUseIntentRef(t *testing.T) {
|
||||
func TestInitiateByQuote_ForwardsClientPaymentRef(t *testing.T) {
|
||||
orgRef := bson.NewObjectID()
|
||||
exec := &fakeExecutionClientForBatch{}
|
||||
api := newBatchAPI(exec)
|
||||
|
||||
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}`
|
||||
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","clientPaymentRef":"client-ref-1"}`
|
||||
rr := invokeInitiateByQuote(t, api, orgRef, body)
|
||||
if got, want := rr.Code, http.StatusOK; got != want {
|
||||
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
|
||||
@@ -32,6 +32,24 @@ func TestInitiateByQuote_DoesNotUseIntentRef(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitiateByQuote_DoesNotForwardLegacyClientPaymentRefFromMetadata(t *testing.T) {
|
||||
orgRef := bson.NewObjectID()
|
||||
exec := &fakeExecutionClientForBatch{}
|
||||
api := newBatchAPI(exec)
|
||||
|
||||
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"client_payment_ref":"legacy-client-ref"}}`
|
||||
rr := invokeInitiateByQuote(t, api, orgRef, body)
|
||||
if got, want := rr.Code, http.StatusOK; got != want {
|
||||
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
|
||||
}
|
||||
if got, want := len(exec.executeReqs), 1; got != want {
|
||||
t.Fatalf("execute calls mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
if got := exec.executeReqs[0].GetClientPaymentRef(); got != "" {
|
||||
t.Fatalf("expected empty client_payment_ref, got=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
|
||||
orgRef := bson.NewObjectID()
|
||||
exec := &fakeExecutionClientForBatch{}
|
||||
@@ -50,7 +68,7 @@ func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
|
||||
func invokeInitiateByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/by-quote", bytes.NewBufferString(body))
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-quote", bytes.NewBufferString(body))
|
||||
routeCtx := chi.NewRouteContext()
|
||||
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
@@ -39,7 +40,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
clientPaymentRef := metadataValue(payload.Metadata, "client_payment_ref")
|
||||
clientPaymentRef := strings.TrimSpace(payload.ClientPaymentRef)
|
||||
idempotencyKey := strings.TrimSpace(payload.IdempotencyKey)
|
||||
quotationRef := strings.TrimSpace(payload.QuoteRef)
|
||||
|
||||
@@ -50,7 +51,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
|
||||
}
|
||||
resp, err := a.execution.ExecuteBatchPayment(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return grpcErrorResponse(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
@@ -62,7 +63,9 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
|
||||
}
|
||||
|
||||
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
|
||||
defer r.Body.Close()
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
|
||||
payload := &srequest.InitiatePayments{}
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
@@ -72,6 +75,7 @@ func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments,
|
||||
}
|
||||
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
|
||||
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
|
||||
payload.ClientPaymentRef = strings.TrimSpace(payload.ClientPaymentRef)
|
||||
|
||||
if err := payload.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
@@ -50,7 +49,7 @@ func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) {
|
||||
exec := &fakeExecutionClientForBatch{}
|
||||
api := newBatchAPI(exec)
|
||||
|
||||
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}`
|
||||
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","clientPaymentRef":"client-ref-1"}`
|
||||
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
|
||||
if got, want := rr.Code, http.StatusOK; got != want {
|
||||
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
|
||||
@@ -67,6 +66,25 @@ func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitiatePaymentsByQuote_DoesNotForwardLegacyClientPaymentRefFromMetadata(t *testing.T) {
|
||||
orgRef := bson.NewObjectID()
|
||||
exec := &fakeExecutionClientForBatch{}
|
||||
api := newBatchAPI(exec)
|
||||
|
||||
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"legacy-client-ref"}}`
|
||||
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
|
||||
if got, want := rr.Code, http.StatusOK; got != want {
|
||||
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
|
||||
}
|
||||
|
||||
if got, want := len(exec.executeBatchReqs), 1; got != want {
|
||||
t.Fatalf("execute batch calls mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
if got := exec.executeBatchReqs[0].GetClientPaymentRef(); got != "" {
|
||||
t.Fatalf("expected empty client_payment_ref, got=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefField(t *testing.T) {
|
||||
orgRef := bson.NewObjectID()
|
||||
exec := &fakeExecutionClientForBatch{}
|
||||
@@ -99,7 +117,7 @@ func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefsField(t *testing.T)
|
||||
|
||||
func newBatchAPI(exec executionClient) *PaymentAPI {
|
||||
return &PaymentAPI{
|
||||
logger: mlogger.Logger(zap.NewNop()),
|
||||
logger: zap.NewNop(),
|
||||
execution: exec,
|
||||
enf: fakeEnforcerForBatch{allowed: true},
|
||||
oph: mutil.CreatePH(mservice.Organizations),
|
||||
@@ -110,7 +128,7 @@ func newBatchAPI(exec executionClient) *PaymentAPI {
|
||||
func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/by-multiquote", bytes.NewBufferString(body))
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-multiquote", bytes.NewBufferString(body))
|
||||
routeCtx := chi.NewRouteContext()
|
||||
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))
|
||||
@@ -158,6 +176,7 @@ func (f fakeEnforcerForBatch) Enforce(context.Context, bson.ObjectID, bson.Objec
|
||||
}
|
||||
|
||||
func (fakeEnforcerForBatch) EnforceBatch(context.Context, []model.PermissionBoundStorable, bson.ObjectID, model.Action) (map[bson.ObjectID]bool, error) {
|
||||
//nolint:nilnil // Test stub does not provide batch permissions map.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
@@ -61,7 +62,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
||||
|
||||
resp, err := a.quotation.QuotePayment(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to quote payment", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||
a.logger.Warn("Failed to quote payment", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return grpcErrorResponse(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
@@ -117,7 +118,7 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
|
||||
|
||||
resp, err := a.quotation.QuotePayments(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||
a.logger.Warn("Failed to quote payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return grpcErrorResponse(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
@@ -125,7 +126,9 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
|
||||
}
|
||||
|
||||
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
|
||||
defer r.Body.Close()
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
|
||||
payload := &srequest.QuotePayment{}
|
||||
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||
@@ -139,7 +142,9 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
|
||||
}
|
||||
|
||||
func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) {
|
||||
defer r.Body.Close()
|
||||
defer func() {
|
||||
_ = r.Body.Close()
|
||||
}()
|
||||
|
||||
payload := &srequest.QuotePayments{}
|
||||
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||
|
||||
@@ -106,7 +106,7 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-multiquote"), api.Post, p.initiatePaymentsByQuote)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listPayments)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/documents/act"), api.Get, p.getActDocument)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/documents/operation"), api.Get, p.getOperationDocument)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/registry"), api.Get, p.listDiscoveryRegistry)
|
||||
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/registry/refresh"), api.Get, p.getDiscoveryRefresh)
|
||||
|
||||
@@ -207,15 +207,12 @@ type grpcQuotationClient struct {
|
||||
callTimeout time.Duration
|
||||
}
|
||||
|
||||
func newQuotationClient(ctx context.Context, cfg quotationClientConfig, opts ...grpc.DialOption) (quotationClient, error) {
|
||||
func newQuotationClient(_ context.Context, cfg quotationClientConfig, opts ...grpc.DialOption) (quotationClient, error) {
|
||||
cfg.setDefaults()
|
||||
if strings.TrimSpace(cfg.Address) == "" {
|
||||
return nil, merrors.InvalidArgument("payment quotation: address is required")
|
||||
}
|
||||
|
||||
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
|
||||
defer cancel()
|
||||
|
||||
dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
|
||||
dialOpts = append(dialOpts, opts...)
|
||||
if cfg.Insecure {
|
||||
@@ -224,7 +221,7 @@ func newQuotationClient(ctx context.Context, cfg quotationClientConfig, opts ...
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
||||
conn, err := grpc.NewClient(cfg.Address, dialOpts...)
|
||||
if err != nil {
|
||||
return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-quotation: dial %s", cfg.Address))
|
||||
}
|
||||
@@ -259,6 +256,7 @@ func (c *grpcQuotationClient) callContext(ctx context.Context) (context.Context,
|
||||
if timeout <= 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
//nolint:gosec // Caller receives cancel func and defers it in every call path.
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
|
||||
@@ -274,7 +272,7 @@ func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name()))
|
||||
client, err := discovery.NewClient(a.logger, broker, nil, a.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,5 +90,5 @@ func (a *PermissionsAPI) changePoliciesImp(
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return struct{}{}, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
@@ -65,24 +66,24 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
|
||||
return response.Auto(a.logger, a.Name(), merrors.NoData("no crypto gateways available"))
|
||||
}
|
||||
a.logger.Debug("Resolved CRYPTO gateways for wallet balance lookup",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.Int("gateway_count", len(cryptoGateways)))
|
||||
|
||||
route, routeErr := a.walletRoute(ctx, orgRef.Hex(), walletRef)
|
||||
if routeErr != nil {
|
||||
a.logger.Warn("Failed to resolve wallet route", zap.Error(routeErr), zap.String("wallet_ref", walletRef), zap.String("organization_ref", orgRef.Hex()))
|
||||
a.logger.Warn("Failed to resolve wallet route", zap.Error(routeErr), zap.String("wallet_ref", walletRef), mzap.ObjRef("organization_ref", orgRef))
|
||||
}
|
||||
if route != nil {
|
||||
a.logger.Debug("Resolved stored wallet route",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("route_network", route.Network),
|
||||
zap.String("route_gateway_id", route.GatewayID))
|
||||
preferred := findGatewayForRoute(cryptoGateways, route)
|
||||
if preferred != nil {
|
||||
a.logger.Debug("Using preferred gateway from stored wallet route",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("gateway_id", preferred.ID),
|
||||
zap.String("network", preferred.Network),
|
||||
@@ -91,7 +92,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
|
||||
if preferredErr == nil && bal != nil {
|
||||
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, preferred.Network, preferred.ID)
|
||||
a.logger.Debug("Wallet balance resolved via preferred gateway",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("gateway_id", preferred.ID),
|
||||
zap.String("network", preferred.Network))
|
||||
@@ -124,20 +125,20 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
|
||||
}
|
||||
} else {
|
||||
a.logger.Warn("Stored wallet route did not match any healthy discovery gateway",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("route_network", route.Network),
|
||||
zap.String("route_gateway_id", route.GatewayID))
|
||||
}
|
||||
} else {
|
||||
a.logger.Debug("Stored wallet route not found; using gateway fallback",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef))
|
||||
}
|
||||
|
||||
// Fall back to querying remaining gateways in parallel.
|
||||
a.logger.Debug("Starting fallback wallet balance fan-out",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.Int("gateway_count", len(cryptoGateways)))
|
||||
bal, err := a.queryBalanceFromGateways(ctx, cryptoGateways, orgRef.Hex(), walletRef)
|
||||
@@ -222,14 +223,11 @@ func (a *WalletAPI) queryBalanceFromGateways(ctx context.Context, gateways []dis
|
||||
a.logger.Debug("Wallet balance fan-out completed without result",
|
||||
zap.String("organization_ref", organizationRef),
|
||||
zap.String("wallet_ref", walletRef))
|
||||
//nolint:nilnil // No gateway returned a balance and no hard error occurred.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.GatewaySummary, walletRef string) (*connectorv1.Balance, error) {
|
||||
// Create connection with timeout
|
||||
dialCtx, cancel := context.WithTimeout(ctx, a.dialTimeout)
|
||||
defer cancel()
|
||||
|
||||
var dialOpts []grpc.DialOption
|
||||
if a.insecure {
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
@@ -237,11 +235,15 @@ func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.G
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(dialCtx, gateway.InvokeURI, dialOpts...)
|
||||
conn, err := grpc.NewClient(gateway.InvokeURI, dialOpts...)
|
||||
if err != nil {
|
||||
return nil, merrors.InternalWrap(err, "dial gateway")
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() {
|
||||
if closeErr := conn.Close(); closeErr != nil {
|
||||
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
|
||||
}
|
||||
}()
|
||||
|
||||
client := connectorv1.NewConnectorServiceClient(conn)
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresp
|
||||
return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("no gateway available for network: "+networkName))
|
||||
}
|
||||
a.logger.Debug("Selected gateway for wallet creation",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("network", networkName),
|
||||
zap.String("gateway_id", gateway.ID),
|
||||
zap.String("gateway_network", gateway.Network),
|
||||
@@ -134,7 +134,7 @@ func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresp
|
||||
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, networkName, gateway.ID)
|
||||
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, gateway.Network, gateway.ID)
|
||||
a.logger.Debug("Persisted wallet route after wallet creation",
|
||||
zap.String("organization_ref", orgRef.Hex()),
|
||||
mzap.ObjRef("organization_ref", orgRef),
|
||||
zap.String("wallet_ref", walletRef),
|
||||
zap.String("network", networkName),
|
||||
zap.String("gateway_id", gateway.ID))
|
||||
@@ -162,10 +162,6 @@ func findGatewayForNetwork(gateways []discovery.GatewaySummary, network string)
|
||||
}
|
||||
|
||||
func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery.GatewaySummary, req *connectorv1.OpenAccountRequest) (string, error) {
|
||||
// Create connection with timeout
|
||||
dialCtx, cancel := context.WithTimeout(ctx, a.dialTimeout)
|
||||
defer cancel()
|
||||
|
||||
var dialOpts []grpc.DialOption
|
||||
if a.insecure {
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
@@ -173,11 +169,15 @@ func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(dialCtx, gateway.InvokeURI, dialOpts...)
|
||||
conn, err := grpc.NewClient(gateway.InvokeURI, dialOpts...)
|
||||
if err != nil {
|
||||
return "", merrors.InternalWrap(err, "dial gateway")
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() {
|
||||
if closeErr := conn.Close(); closeErr != nil {
|
||||
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
|
||||
}
|
||||
}()
|
||||
|
||||
client := connectorv1.NewConnectorServiceClient(conn)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user