Merge branch 'main' into SEND066
This commit is contained in:
@@ -7,6 +7,9 @@ matrix:
|
||||
BFF_VAULT_SECRET_PATH: sendico/edge/bff/vault
|
||||
BFF_ENV: prod
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -53,7 +56,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -87,6 +90,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -48,7 +51,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -82,6 +85,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -48,7 +51,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -82,6 +85,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -49,7 +52,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -83,6 +86,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -47,7 +50,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -81,6 +84,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -53,7 +56,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -87,6 +90,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -54,7 +57,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -88,6 +91,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -52,7 +55,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -86,6 +89,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -51,7 +54,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -85,6 +88,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -49,7 +52,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -83,6 +86,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -52,7 +55,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -86,6 +89,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -48,7 +51,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -82,6 +85,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -51,7 +54,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -85,6 +88,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -49,7 +52,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -83,6 +86,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -49,7 +52,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -83,6 +86,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
@@ -49,7 +52,7 @@ steps:
|
||||
commands:
|
||||
- set -eu
|
||||
- apk add --no-cache bash git build-base
|
||||
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
- 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
|
||||
|
||||
@@ -83,6 +86,7 @@ steps:
|
||||
image: gcr.io/kaniko-project/executor:debug
|
||||
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
|
||||
|
||||
104
Makefile
104
Makefile
@@ -1,12 +1,48 @@
|
||||
# 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 lint-api backend-up backend-down backend-rebuild
|
||||
.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 ?=
|
||||
API_GOCACHE ?= $(CURDIR)/.gocache
|
||||
API_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache
|
||||
BACKEND_GOCACHE ?= $(CURDIR)/.gocache
|
||||
BACKEND_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache
|
||||
BACKEND_SERVICES := \
|
||||
dev-discovery \
|
||||
dev-fx-oracle \
|
||||
@@ -47,8 +83,8 @@ 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)"
|
||||
@@ -62,23 +98,23 @@ help:
|
||||
@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-payments Build payment services (orchestrator, quotation, methods)"
|
||||
@echo " make build-gateways Build gateway services (chain, tron, aurora, chsettle)"
|
||||
@echo " make build-api Build API services (notification, callbacks, bff)"
|
||||
@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-api Run golangci-lint across all API Go modules"
|
||||
@echo " make lint-backend Run golangci-lint across all backend Go modules"
|
||||
@echo " make health Check service health"
|
||||
@echo ""
|
||||
@echo "Examples:"
|
||||
@@ -140,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
|
||||
@@ -164,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)
|
||||
@@ -173,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)"
|
||||
@@ -327,8 +363,8 @@ build-gateways:
|
||||
@echo "$(GREEN)Building gateway services...$(NC)"
|
||||
@$(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:
|
||||
@@ -336,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..."; \
|
||||
@@ -355,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..."; \
|
||||
@@ -369,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:
|
||||
@@ -378,17 +414,17 @@ test-frontend:
|
||||
@cd frontend/pweb && flutter test
|
||||
@echo "$(GREEN)✅ All frontend tests passed$(NC)"
|
||||
|
||||
# Run Go API linting
|
||||
lint-api:
|
||||
@echo "$(GREEN)Running API linting...$(NC)"
|
||||
@mkdir -p "$(API_GOCACHE)" "$(API_GOLANGCI_LINT_CACHE)"
|
||||
# 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="$(API_GOCACHE)" GOLANGCI_LINT_CACHE="$(API_GOLANGCI_LINT_CACHE)" golangci-lint run --allow-serial-runners --allow-parallel-runners ./...) || failed="$$failed $$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 API lint checks passed$(NC)"
|
||||
@echo "$(GREEN)✅ All backend lint checks passed$(NC)"
|
||||
|
||||
12
README.md
12
README.md
@@ -89,7 +89,7 @@ make build-core # discovery, ledger, fees, documents
|
||||
make build-fx # oracle, ingestor
|
||||
make build-payments # orchestrator, quotation, methods
|
||||
make build-gateways # chain, tron, aurora, chsettle
|
||||
make build-api # notification, callbacks, bff
|
||||
make build-backend # notification, callbacks, bff
|
||||
make build-frontend # Flutter web UI
|
||||
```
|
||||
|
||||
@@ -97,16 +97,16 @@ 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
|
||||
```
|
||||
|
||||
@@ -133,7 +133,7 @@ If you intentionally need to bypass checks for a specific commit, include one of
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -147,11 +147,11 @@ 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.41.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.41.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.41.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.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
|
||||
@@ -296,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.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
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.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
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.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
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=
|
||||
|
||||
@@ -14,8 +14,11 @@ import (
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -67,16 +70,24 @@ type PaymentQuotes struct {
|
||||
}
|
||||
|
||||
type Payment struct {
|
||||
PaymentRef string `json:"paymentRef,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Comment string `json:"comment,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 {
|
||||
@@ -290,22 +301,257 @@ func toPayment(p *orchestrationv2.Payment) *Payment {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
operations := toUserVisibleOperations(p.GetStepExecutions(), p.GetQuoteSnapshot())
|
||||
intent := p.GetIntentSnapshot()
|
||||
operations := toUserVisibleOperations(p.GetStepExecutions())
|
||||
failureCode, failureReason := firstFailure(operations)
|
||||
return &Payment{
|
||||
PaymentRef: p.GetPaymentRef(),
|
||||
State: enumJSONName(p.GetState().String()),
|
||||
Comment: strings.TrimSpace(p.GetIntentSnapshot().GetComment()),
|
||||
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) == "" {
|
||||
@@ -316,7 +562,7 @@ func firstFailure(operations []PaymentOperation) (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func toUserVisibleOperations(steps []*orchestrationv2.StepExecution, quote *quotationv2.PaymentQuote) []PaymentOperation {
|
||||
func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOperation {
|
||||
if len(steps) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -325,7 +571,7 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution, quote *quot
|
||||
if step == nil || !isUserVisibleStep(step.GetReportVisibility()) {
|
||||
continue
|
||||
}
|
||||
ops = append(ops, toPaymentOperation(step, quote))
|
||||
ops = append(ops, toPaymentOperation(step))
|
||||
}
|
||||
if len(ops) == 0 {
|
||||
return nil
|
||||
@@ -333,9 +579,10 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution, quote *quot
|
||||
return ops
|
||||
}
|
||||
|
||||
func toPaymentOperation(step *orchestrationv2.StepExecution, quote *quotationv2.PaymentQuote) PaymentOperation {
|
||||
func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
|
||||
operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs())
|
||||
amount, convertedAmount := operationAmounts(step.GetStepCode(), quote)
|
||||
amount := normalizeOperationMoney(toMoney(step.GetExecutedMoney()))
|
||||
convertedAmount := normalizeOperationMoney(toMoney(step.GetConvertedMoney()))
|
||||
op := PaymentOperation{
|
||||
StepRef: step.GetStepRef(),
|
||||
Code: step.GetStepCode(),
|
||||
@@ -360,52 +607,19 @@ func toPaymentOperation(step *orchestrationv2.StepExecution, quote *quotationv2.
|
||||
return op
|
||||
}
|
||||
|
||||
func operationAmounts(stepCode string, quote *quotationv2.PaymentQuote) (*paymenttypes.Money, *paymenttypes.Money) {
|
||||
if quote == nil {
|
||||
return nil, nil
|
||||
func normalizeOperationMoney(value *paymenttypes.Money) *paymenttypes.Money {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
operation := stepOperationToken(stepCode)
|
||||
|
||||
primary := firstValidMoney(
|
||||
toMoney(quote.GetDestinationAmount()),
|
||||
toMoney(quote.GetTransferPrincipalAmount()),
|
||||
toMoney(quote.GetPayerTotalDebitAmount()),
|
||||
)
|
||||
if operation != "fx_convert" {
|
||||
return primary, nil
|
||||
amount := strings.TrimSpace(value.GetAmount())
|
||||
currency := strings.TrimSpace(value.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
base := firstValidMoney(
|
||||
toMoney(quote.GetTransferPrincipalAmount()),
|
||||
toMoney(quote.GetPayerTotalDebitAmount()),
|
||||
toMoney(quote.GetFxQuote().GetBaseAmount()),
|
||||
)
|
||||
quoteAmount := firstValidMoney(
|
||||
toMoney(quote.GetDestinationAmount()),
|
||||
toMoney(quote.GetFxQuote().GetQuoteAmount()),
|
||||
)
|
||||
return base, quoteAmount
|
||||
}
|
||||
|
||||
func stepOperationToken(stepCode string) string {
|
||||
parts := strings.Split(strings.ToLower(strings.TrimSpace(stepCode)), ".")
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
return strings.TrimSpace(parts[len(parts)-1])
|
||||
}
|
||||
|
||||
func firstValidMoney(values ...*paymenttypes.Money) *paymenttypes.Money {
|
||||
for _, value := range values {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(value.GetAmount()) == "" || strings.TrimSpace(value.GetCurrency()) == "" {
|
||||
continue
|
||||
}
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
|
||||
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) {
|
||||
@@ -34,7 +37,7 @@ func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ops := toUserVisibleOperations(steps, nil)
|
||||
ops := toUserVisibleOperations(steps)
|
||||
if len(ops) != 2 {
|
||||
t.Fatalf("operations count mismatch: got=%d want=2", len(ops))
|
||||
}
|
||||
@@ -137,6 +140,125 @@ func TestToPaymentMapsIntentComment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -166,7 +288,7 @@ func TestToPaymentOperation_MapsOperationRefAndGateway(t *testing.T) {
|
||||
Ref: "op-123",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
})
|
||||
|
||||
if got, want := op.OperationRef, "op-123"; got != want {
|
||||
t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want)
|
||||
@@ -181,7 +303,7 @@ func TestToPaymentOperation_InfersGatewayFromStepCode(t *testing.T) {
|
||||
StepRef: "step-2",
|
||||
StepCode: "edge.1_2.ledger.debit",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
}, nil)
|
||||
})
|
||||
|
||||
if got := op.OperationRef; got != "" {
|
||||
t.Fatalf("expected empty operation_ref, got=%q", got)
|
||||
@@ -204,7 +326,7 @@ func TestToPaymentOperation_DoesNotFallbackToCardPayoutRef(t *testing.T) {
|
||||
Ref: "payout-123",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
})
|
||||
|
||||
if got := op.OperationRef; got != "" {
|
||||
t.Fatalf("expected empty operation_ref, got=%q", got)
|
||||
@@ -219,15 +341,28 @@ func TestToPaymentOperation_MapsAmount(t *testing.T) {
|
||||
StepRef: "step-4",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
|
||||
}, "ationv2.PaymentQuote{
|
||||
TransferPrincipalAmount: &moneyv1.Money{Amount: "110.00", Currency: "USDT"},
|
||||
DestinationAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
})
|
||||
|
||||
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, "100.00"; got != want {
|
||||
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 {
|
||||
@@ -240,22 +375,14 @@ func TestToPaymentOperation_MapsAmount(t *testing.T) {
|
||||
|
||||
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,
|
||||
}, "ationv2.PaymentQuote{
|
||||
TransferPrincipalAmount: &moneyv1.Money{Amount: "110.00", Currency: "USDT"},
|
||||
DestinationAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
|
||||
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 op.Amount == nil {
|
||||
t.Fatal("expected fx base amount to be mapped")
|
||||
}
|
||||
if got, want := op.Amount.Amount, "110.00"; 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 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")
|
||||
@@ -267,3 +394,32 @@ func TestToPaymentOperation_MapsFxTwoAmounts(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +152,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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.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=
|
||||
|
||||
@@ -13,14 +13,38 @@ Aurora is a dev/test-only card payout gateway with the same gRPC contract as `mn
|
||||
- No outbound payout/tokenization HTTP calls are made.
|
||||
|
||||
## Built-in test cards
|
||||
- `2200001111111111`: approved instantly (`success`, code `00`)
|
||||
- `2200002222222222`: pending issuer review (`waiting`, code `P01`)
|
||||
- `2200003333333333`: insufficient funds (`failed`, code `51`)
|
||||
- `2200004444444444`: issuer unavailable retryable (`failed`, code `10101`)
|
||||
- `2200005555555555`: stolen card (`failed`, code `43`)
|
||||
- `2200006666666666`: do not honor (`failed`, code `05`)
|
||||
- `2200007777777777`: expired card (`failed`, code `54`)
|
||||
- any other PAN: default queued processing (`waiting`, code `P00`)
|
||||
- Every scenario has multiple generated PANs (8 per case), all Luhn-valid.
|
||||
- PANs are generated from scenario prefixes in [`scenario_simulator.go`](./internal/service/gateway/scenario_simulator.go) via `generatePANSeriesWithLuhn`.
|
||||
- Scenario prefixes:
|
||||
- `22000011`: approved instantly (`success`, code `00`)
|
||||
- `22000022`: pending issuer review (`waiting`, code `P01`)
|
||||
- `22000033`: insufficient funds (`failed`, code `51`)
|
||||
- `22000044`: issuer unavailable retryable (`failed`, code `10101`)
|
||||
- `22000055`: stolen card (`failed`, code `43`)
|
||||
- `22000066`: do not honor (`failed`, code `05`)
|
||||
- `22000077`: expired card (`failed`, code `54`)
|
||||
- `22000088`: provider timeout transport error
|
||||
- `22000098`: provider unreachable transport error
|
||||
- `22000097`: provider maintenance (`failed`, code `91`)
|
||||
- `22000096`: provider system malfunction (`failed`, code `96`)
|
||||
- Any other valid PAN defaults to queued processing (`waiting`, code `P00`).
|
||||
|
||||
### Auxiliary PAN Table (Generated, Luhn-valid)
|
||||
|
||||
| Scenario | Prefix | PANs |
|
||||
|---|---|---|
|
||||
| approved_instant | `22000011` | `2200001100000001`<br>`2200001100000019`<br>`2200001100000027`<br>`2200001100000035`<br>`2200001100000043`<br>`2200001100000050`<br>`2200001100000068`<br>`2200001100000076` |
|
||||
| pending_issuer_review | `22000022` | `2200002200000008`<br>`2200002200000016`<br>`2200002200000024`<br>`2200002200000032`<br>`2200002200000040`<br>`2200002200000057`<br>`2200002200000065`<br>`2200002200000073` |
|
||||
| insufficient_funds | `22000033` | `2200003300000005`<br>`2200003300000013`<br>`2200003300000021`<br>`2200003300000039`<br>`2200003300000047`<br>`2200003300000054`<br>`2200003300000062`<br>`2200003300000070` |
|
||||
| issuer_unavailable_retryable | `22000044` | `2200004400000002`<br>`2200004400000010`<br>`2200004400000028`<br>`2200004400000036`<br>`2200004400000044`<br>`2200004400000051`<br>`2200004400000069`<br>`2200004400000077` |
|
||||
| stolen_card | `22000055` | `2200005500000008`<br>`2200005500000016`<br>`2200005500000024`<br>`2200005500000032`<br>`2200005500000040`<br>`2200005500000057`<br>`2200005500000065`<br>`2200005500000073` |
|
||||
| do_not_honor | `22000066` | `2200006600000005`<br>`2200006600000013`<br>`2200006600000021`<br>`2200006600000039`<br>`2200006600000047`<br>`2200006600000054`<br>`2200006600000062`<br>`2200006600000070` |
|
||||
| expired_card | `22000077` | `2200007700000002`<br>`2200007700000010`<br>`2200007700000028`<br>`2200007700000036`<br>`2200007700000044`<br>`2200007700000051`<br>`2200007700000069`<br>`2200007700000077` |
|
||||
| provider_timeout_transport | `22000088` | `2200008800000009`<br>`2200008800000017`<br>`2200008800000025`<br>`2200008800000033`<br>`2200008800000041`<br>`2200008800000058`<br>`2200008800000066`<br>`2200008800000074` |
|
||||
| provider_unreachable_transport | `22000098` | `2200009800000007`<br>`2200009800000015`<br>`2200009800000023`<br>`2200009800000031`<br>`2200009800000049`<br>`2200009800000056`<br>`2200009800000064`<br>`2200009800000072` |
|
||||
| provider_maintenance | `22000097` | `2200009700000008`<br>`2200009700000016`<br>`2200009700000024`<br>`2200009700000032`<br>`2200009700000040`<br>`2200009700000057`<br>`2200009700000065`<br>`2200009700000073` |
|
||||
| provider_system_malfunction | `22000096` | `2200009600000009`<br>`2200009600000017`<br>`2200009600000025`<br>`2200009600000033`<br>`2200009600000041`<br>`2200009600000058`<br>`2200009600000066`<br>`2200009600000074` |
|
||||
| default_processing (example) | `22000999` | `2200099900000007` |
|
||||
|
||||
## Notes
|
||||
- PAN is masked in logs.
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "approved_instant",
|
||||
pan: "2200001111111111",
|
||||
pan: scenarioPAN("approved_instant", 0),
|
||||
wantAccepted: true,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS,
|
||||
wantErrorCode: "00",
|
||||
@@ -42,7 +42,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "pending_issuer_review",
|
||||
pan: "2200002222222222",
|
||||
pan: scenarioPAN("pending_issuer_review", 0),
|
||||
wantAccepted: true,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
|
||||
wantErrorCode: "P01",
|
||||
@@ -51,7 +51,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "insufficient_funds",
|
||||
pan: "2200003333333333",
|
||||
pan: scenarioPAN("insufficient_funds", 0),
|
||||
wantAccepted: false,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
|
||||
wantErrorCode: "51",
|
||||
@@ -60,7 +60,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "unknown_card_default_queue",
|
||||
pan: "2200009999999999",
|
||||
pan: defaultScenarioPAN(),
|
||||
wantAccepted: true,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING,
|
||||
wantErrorCode: "P00",
|
||||
@@ -69,7 +69,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "provider_maintenance",
|
||||
pan: "2200009999999997",
|
||||
pan: scenarioPAN("provider_maintenance", 0),
|
||||
wantAccepted: false,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
|
||||
wantErrorCode: "91",
|
||||
@@ -78,7 +78,7 @@ func TestAuroraCardPayoutScenarios(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "provider_system_malfunction",
|
||||
pan: "2200009999999996",
|
||||
pan: scenarioPAN("provider_system_malfunction", 0),
|
||||
wantAccepted: false,
|
||||
wantStatus: mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED,
|
||||
wantErrorCode: "96",
|
||||
@@ -148,7 +148,7 @@ func TestAuroraTransportFailureScenarioEventuallyFails(t *testing.T) {
|
||||
req.OperationRef = "op-transport-timeout"
|
||||
req.ParentPaymentRef = "parent-transport-timeout"
|
||||
req.IdempotencyKey = "idem-transport-timeout"
|
||||
req.CardPan = "2200008888888888"
|
||||
req.CardPan = scenarioPAN("provider_timeout_transport", 0)
|
||||
|
||||
resp, err := processor.Submit(context.Background(), req)
|
||||
if err != nil {
|
||||
@@ -201,7 +201,7 @@ func TestAuroraRetryableScenarioEventuallyFails(t *testing.T) {
|
||||
req.OperationRef = "op-retryable-issuer-unavailable"
|
||||
req.ParentPaymentRef = "parent-retryable-issuer-unavailable"
|
||||
req.IdempotencyKey = "idem-retryable-issuer-unavailable"
|
||||
req.CardPan = "2200004444444444"
|
||||
req.CardPan = scenarioPAN("issuer_unavailable_retryable", 0)
|
||||
|
||||
resp, err := processor.Submit(context.Background(), req)
|
||||
if err != nil {
|
||||
@@ -248,7 +248,7 @@ func TestAuroraTokenPayoutUsesTokenizedPANScenario(t *testing.T) {
|
||||
|
||||
tokenizeReq := validCardTokenizeRequest()
|
||||
tokenizeReq.RequestId = "tok-req-insufficient"
|
||||
tokenizeReq.CardPan = "2200003333333333"
|
||||
tokenizeReq.CardPan = scenarioPAN("insufficient_funds", 0)
|
||||
|
||||
tokenizeResp, err := processor.Tokenize(context.Background(), tokenizeReq)
|
||||
if err != nil {
|
||||
|
||||
149
api/gateway/aurora/internal/service/gateway/card_pan.go
Normal file
149
api/gateway/aurora/internal/service/gateway/card_pan.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
const (
|
||||
minCardPANLength = 12
|
||||
maxCardPANLength = 19
|
||||
)
|
||||
|
||||
func normalizeCardNumber(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(value))
|
||||
for _, r := range value {
|
||||
if r >= '0' && r <= '9' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func validateCardPAN(pan string, field string) error {
|
||||
normalized, err := normalizedPANForValidation(pan, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isValidPANLuhn(normalized) {
|
||||
return merrors.InvalidArgument("card_pan checksum is invalid", field)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedPANForValidation(pan string, field string) (string, error) {
|
||||
field = strings.TrimSpace(field)
|
||||
if field == "" {
|
||||
field = "card_pan"
|
||||
}
|
||||
|
||||
clean := strings.TrimSpace(pan)
|
||||
if clean == "" {
|
||||
return "", merrors.InvalidArgument("card_pan is required", field)
|
||||
}
|
||||
|
||||
compact := strings.NewReplacer(" ", "", "-", "").Replace(clean)
|
||||
if compact == "" {
|
||||
return "", merrors.InvalidArgument("card_pan must contain only digits (spaces/hyphens allowed)", field)
|
||||
}
|
||||
|
||||
normalized := normalizeCardNumber(compact)
|
||||
if normalized != compact {
|
||||
return "", merrors.InvalidArgument("card_pan must contain only digits (spaces/hyphens allowed)", field)
|
||||
}
|
||||
if len(normalized) < minCardPANLength || len(normalized) > maxCardPANLength {
|
||||
return "", merrors.InvalidArgument(
|
||||
fmt.Sprintf("card_pan length must be between %d and %d", minCardPANLength, maxCardPANLength),
|
||||
field,
|
||||
)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func isValidPANLuhn(number string) bool {
|
||||
if len(number) < minCardPANLength || len(number) > maxCardPANLength {
|
||||
return false
|
||||
}
|
||||
|
||||
sum := 0
|
||||
double := false
|
||||
for i := len(number) - 1; i >= 0; i-- {
|
||||
d := int(number[i] - '0')
|
||||
if d < 0 || d > 9 {
|
||||
return false
|
||||
}
|
||||
if double {
|
||||
d *= 2
|
||||
if d > 9 {
|
||||
d -= 9
|
||||
}
|
||||
}
|
||||
sum += d
|
||||
double = !double
|
||||
}
|
||||
return sum%10 == 0
|
||||
}
|
||||
|
||||
func generatePANWithLuhn(prefix string, sequence uint64, panLength int) (string, error) {
|
||||
prefix = normalizeCardNumber(prefix)
|
||||
if prefix == "" {
|
||||
return "", merrors.InvalidArgument("prefix is required", "prefix")
|
||||
}
|
||||
if panLength < minCardPANLength || panLength > maxCardPANLength {
|
||||
return "", merrors.InvalidArgument(
|
||||
fmt.Sprintf("pan_length must be between %d and %d", minCardPANLength, maxCardPANLength),
|
||||
"pan_length",
|
||||
)
|
||||
}
|
||||
|
||||
bodyLen := panLength - 1
|
||||
if len(prefix) > bodyLen {
|
||||
return "", merrors.InvalidArgument("prefix is too long for selected pan_length", "prefix")
|
||||
}
|
||||
|
||||
seqWidth := bodyLen - len(prefix)
|
||||
seqMod := pow10(seqWidth)
|
||||
body := prefix + fmt.Sprintf("%0*d", seqWidth, sequence%seqMod)
|
||||
|
||||
for checksum := 0; checksum <= 9; checksum++ {
|
||||
candidate := body + strconv.Itoa(checksum)
|
||||
if isValidPANLuhn(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", merrors.Internal("failed to generate PAN checksum digit")
|
||||
}
|
||||
|
||||
func generatePANSeriesWithLuhn(prefix string, count int, panLength int) ([]string, error) {
|
||||
if count <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]string, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
pan, err := generatePANWithLuhn(prefix, uint64(i), panLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, pan)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func pow10(n int) uint64 {
|
||||
if n <= 0 {
|
||||
return 1
|
||||
}
|
||||
p := uint64(1)
|
||||
for i := 0; i < n; i++ {
|
||||
p *= 10
|
||||
}
|
||||
return p
|
||||
}
|
||||
36
api/gateway/aurora/internal/service/gateway/card_pan_test.go
Normal file
36
api/gateway/aurora/internal/service/gateway/card_pan_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package gateway
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateCardPAN_AllowsFormattedInput(t *testing.T) {
|
||||
if err := validateCardPAN("4111 1111-1111 1111", "card_pan"); err != nil {
|
||||
t.Fatalf("expected formatted PAN to pass validation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCardPAN_RejectsInvalidChecksum(t *testing.T) {
|
||||
if err := validateCardPAN("4111111111111112", "card_pan"); err == nil {
|
||||
t.Fatal("expected invalid checksum to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePANSeriesWithLuhn_ProducesValidUniquePANs(t *testing.T) {
|
||||
pans, err := generatePANSeriesWithLuhn("22000033", 6, 16)
|
||||
if err != nil {
|
||||
t.Fatalf("generatePANSeriesWithLuhn returned error: %v", err)
|
||||
}
|
||||
if len(pans) != 6 {
|
||||
t.Fatalf("unexpected PAN count: got=%d want=6", len(pans))
|
||||
}
|
||||
|
||||
seen := map[string]struct{}{}
|
||||
for _, pan := range pans {
|
||||
if !isValidPANLuhn(pan) {
|
||||
t.Fatalf("generated PAN is not Luhn-valid: %q", pan)
|
||||
}
|
||||
if _, ok := seen[pan]; ok {
|
||||
t.Fatalf("duplicate PAN generated: %q", pan)
|
||||
}
|
||||
seen[pan] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ func validateCardPayoutRequest(req *mntxv1.CardPayoutRequest, cfg provider.Confi
|
||||
if pan == "" {
|
||||
return newPayoutError("missing_card_pan", merrors.InvalidArgument("card_pan is required", "card_pan"))
|
||||
}
|
||||
if err := validateCardPAN(pan, "card_pan"); err != nil {
|
||||
return newPayoutError("invalid_card_pan_crc", err)
|
||||
}
|
||||
if strings.TrimSpace(req.GetCardHolder()) == "" {
|
||||
return newPayoutError("missing_card_holder", merrors.InvalidArgument("card_holder is required", "card_holder"))
|
||||
}
|
||||
|
||||
@@ -75,6 +75,11 @@ func TestValidateCardPayoutRequest_Errors(t *testing.T) {
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardPan = "" },
|
||||
expected: "missing_card_pan",
|
||||
},
|
||||
{
|
||||
name: "invalid_card_pan_crc",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardPan = "4111111111111112" },
|
||||
expected: "invalid_card_pan_crc",
|
||||
},
|
||||
{
|
||||
name: "missing_card_holder",
|
||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.CardHolder = "" },
|
||||
|
||||
@@ -295,14 +295,14 @@ func TestCardPayoutProcessor_Submit_SameParentDifferentOperationsStoredSeparatel
|
||||
req1.OperationRef = op1
|
||||
req1.IdempotencyKey = "idem-1"
|
||||
req1.ParentPaymentRef = parentPaymentRef
|
||||
req1.CardPan = "2204310000002456"
|
||||
req1.CardPan = scenarioPAN("approved_instant", 1)
|
||||
|
||||
req2 := validCardPayoutRequest()
|
||||
req2.PayoutId = ""
|
||||
req2.OperationRef = op2
|
||||
req2.IdempotencyKey = "idem-2"
|
||||
req2.ParentPaymentRef = parentPaymentRef
|
||||
req2.CardPan = "2204320000009754"
|
||||
req2.CardPan = scenarioPAN("pending_issuer_review", 1)
|
||||
|
||||
if _, err := processor.Submit(context.Background(), req1); err != nil {
|
||||
t.Fatalf("first submit failed: %v", err)
|
||||
@@ -385,14 +385,14 @@ func TestCardPayoutProcessor_StrictMode_BlocksSecondOperationUntilFirstFinalCall
|
||||
req1.OperationRef = "op-strict-1"
|
||||
req1.ParentPaymentRef = "payment-strict-1"
|
||||
req1.IdempotencyKey = "idem-strict-1"
|
||||
req1.CardPan = "2204310000002456"
|
||||
req1.CardPan = scenarioPAN("approved_instant", 2)
|
||||
|
||||
req2 := validCardPayoutRequest()
|
||||
req2.PayoutId = ""
|
||||
req2.OperationRef = "op-strict-2"
|
||||
req2.ParentPaymentRef = "payment-strict-2"
|
||||
req2.IdempotencyKey = "idem-strict-2"
|
||||
req2.CardPan = "2204320000009754"
|
||||
req2.CardPan = scenarioPAN("pending_issuer_review", 2)
|
||||
|
||||
if _, err := processor.Submit(context.Background(), req1); err != nil {
|
||||
t.Fatalf("first submit failed: %v", err)
|
||||
|
||||
@@ -41,6 +41,9 @@ func validateCardTokenizeRequest(req *mntxv1.CardTokenizeRequest, cfg provider.C
|
||||
if card.pan == "" {
|
||||
return nil, newPayoutError("missing_card_pan", merrors.InvalidArgument("card_pan is required", "card.pan"))
|
||||
}
|
||||
if err := validateCardPAN(card.pan, "card.pan"); err != nil {
|
||||
return nil, newPayoutError("invalid_card_pan_crc", err)
|
||||
}
|
||||
if card.holder == "" {
|
||||
return nil, newPayoutError("missing_card_holder", merrors.InvalidArgument("card_holder is required", "card.holder"))
|
||||
}
|
||||
|
||||
@@ -65,6 +65,15 @@ func TestValidateCardTokenizeRequest_MissingCardPan(t *testing.T) {
|
||||
requireReason(t, err, "missing_card_pan")
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_InvalidCardPanCRC(t *testing.T) {
|
||||
cfg := testProviderConfig()
|
||||
req := validCardTokenizeRequest()
|
||||
req.CardPan = "4111111111111112"
|
||||
|
||||
_, err := validateCardTokenizeRequest(req, cfg)
|
||||
requireReason(t, err, "invalid_card_pan_crc")
|
||||
}
|
||||
|
||||
func TestValidateCardTokenizeRequest_AddressRequired(t *testing.T) {
|
||||
cfg := testProviderConfig()
|
||||
cfg.RequireCustomerAddress = true
|
||||
|
||||
@@ -28,12 +28,31 @@ type payoutSimulator struct {
|
||||
seq atomic.Uint64
|
||||
}
|
||||
|
||||
const (
|
||||
scenarioPANLength = 16
|
||||
scenarioPANVariants = 8
|
||||
)
|
||||
|
||||
var scenarioPANPrefixes = map[string]string{
|
||||
"approved_instant": "22000011",
|
||||
"pending_issuer_review": "22000022",
|
||||
"insufficient_funds": "22000033",
|
||||
"issuer_unavailable_retryable": "22000044",
|
||||
"stolen_card": "22000055",
|
||||
"do_not_honor": "22000066",
|
||||
"expired_card": "22000077",
|
||||
"provider_timeout_transport": "22000088",
|
||||
"provider_unreachable_transport": "22000098",
|
||||
"provider_maintenance": "22000097",
|
||||
"provider_system_malfunction": "22000096",
|
||||
}
|
||||
|
||||
func newPayoutSimulator() *payoutSimulator {
|
||||
return &payoutSimulator{
|
||||
scenarios: []simulatedCardScenario{
|
||||
{
|
||||
Name: "approved_instant",
|
||||
CardNumbers: []string{"2200001111111111"},
|
||||
CardNumbers: mustScenarioPANs("approved_instant"),
|
||||
Accepted: true,
|
||||
ProviderStatus: "success",
|
||||
ErrorCode: "00",
|
||||
@@ -41,7 +60,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "pending_issuer_review",
|
||||
CardNumbers: []string{"2200002222222222"},
|
||||
CardNumbers: mustScenarioPANs("pending_issuer_review"),
|
||||
Accepted: true,
|
||||
ProviderStatus: "processing",
|
||||
ErrorCode: "P01",
|
||||
@@ -49,7 +68,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "insufficient_funds",
|
||||
CardNumbers: []string{"2200003333333333"},
|
||||
CardNumbers: mustScenarioPANs("insufficient_funds"),
|
||||
CardLast4: []string{"3333"},
|
||||
Accepted: false,
|
||||
ErrorCode: "51",
|
||||
@@ -57,7 +76,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "issuer_unavailable_retryable",
|
||||
CardNumbers: []string{"2200004444444444"},
|
||||
CardNumbers: mustScenarioPANs("issuer_unavailable_retryable"),
|
||||
CardLast4: []string{"4444"},
|
||||
Accepted: false,
|
||||
ErrorCode: "10101",
|
||||
@@ -65,7 +84,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "stolen_card",
|
||||
CardNumbers: []string{"2200005555555555"},
|
||||
CardNumbers: mustScenarioPANs("stolen_card"),
|
||||
CardLast4: []string{"5555"},
|
||||
Accepted: false,
|
||||
ErrorCode: "43",
|
||||
@@ -73,7 +92,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "do_not_honor",
|
||||
CardNumbers: []string{"2200006666666666"},
|
||||
CardNumbers: mustScenarioPANs("do_not_honor"),
|
||||
CardLast4: []string{"6666"},
|
||||
Accepted: false,
|
||||
ErrorCode: "05",
|
||||
@@ -81,7 +100,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "expired_card",
|
||||
CardNumbers: []string{"2200007777777777"},
|
||||
CardNumbers: mustScenarioPANs("expired_card"),
|
||||
CardLast4: []string{"7777"},
|
||||
Accepted: false,
|
||||
ErrorCode: "54",
|
||||
@@ -89,19 +108,19 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "provider_timeout_transport",
|
||||
CardNumbers: []string{"2200008888888888"},
|
||||
CardNumbers: mustScenarioPANs("provider_timeout_transport"),
|
||||
CardLast4: []string{"8888"},
|
||||
DispatchError: "provider timeout while calling payout endpoint",
|
||||
},
|
||||
{
|
||||
Name: "provider_unreachable_transport",
|
||||
CardNumbers: []string{"2200009999999998"},
|
||||
CardNumbers: mustScenarioPANs("provider_unreachable_transport"),
|
||||
CardLast4: []string{"9998"},
|
||||
DispatchError: "provider host unreachable",
|
||||
},
|
||||
{
|
||||
Name: "provider_maintenance",
|
||||
CardNumbers: []string{"2200009999999997"},
|
||||
CardNumbers: mustScenarioPANs("provider_maintenance"),
|
||||
CardLast4: []string{"9997"},
|
||||
Accepted: false,
|
||||
ErrorCode: "91",
|
||||
@@ -109,7 +128,7 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
},
|
||||
{
|
||||
Name: "provider_system_malfunction",
|
||||
CardNumbers: []string{"2200009999999996"},
|
||||
CardNumbers: mustScenarioPANs("provider_system_malfunction"),
|
||||
CardLast4: []string{"9996"},
|
||||
Accepted: false,
|
||||
ErrorCode: "96",
|
||||
@@ -126,6 +145,45 @@ func newPayoutSimulator() *payoutSimulator {
|
||||
}
|
||||
}
|
||||
|
||||
func mustScenarioPANs(name string) []string {
|
||||
pans := scenarioPANs(name)
|
||||
if len(pans) == 0 {
|
||||
panic("aurora simulator scenario pan generation failed for " + name)
|
||||
}
|
||||
return pans
|
||||
}
|
||||
|
||||
func scenarioPANs(name string) []string {
|
||||
prefix := strings.TrimSpace(scenarioPANPrefixes[name])
|
||||
if prefix == "" {
|
||||
return nil
|
||||
}
|
||||
pans, err := generatePANSeriesWithLuhn(prefix, scenarioPANVariants, scenarioPANLength)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return pans
|
||||
}
|
||||
|
||||
func scenarioPAN(name string, index int) string {
|
||||
pans := scenarioPANs(name)
|
||||
if len(pans) == 0 {
|
||||
return ""
|
||||
}
|
||||
if index < 0 {
|
||||
index = 0
|
||||
}
|
||||
return pans[index%len(pans)]
|
||||
}
|
||||
|
||||
func defaultScenarioPAN() string {
|
||||
pan, err := generatePANWithLuhn("22000999", 0, scenarioPANLength)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return pan
|
||||
}
|
||||
|
||||
func (s *payoutSimulator) resolveByPAN(pan string) simulatedCardScenario {
|
||||
return s.resolve(normalizeCardNumber(pan), "")
|
||||
}
|
||||
@@ -210,21 +268,6 @@ func scenarioMatchesLast4(scenario simulatedCardScenario, last4 string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeCardNumber(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(value))
|
||||
for _, r := range value {
|
||||
if r >= '0' && r <= '9' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func normalizeExpiryYear(year uint32) string {
|
||||
if year == 0 {
|
||||
return ""
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func TestPayoutSimulatorResolveByPAN_KnownCard(t *testing.T) {
|
||||
sim := newPayoutSimulator()
|
||||
|
||||
scenario := sim.resolveByPAN("2200003333333333")
|
||||
scenario := sim.resolveByPAN(scenarioPAN("insufficient_funds", 0))
|
||||
if scenario.Name != "insufficient_funds" {
|
||||
t.Fatalf("unexpected scenario: got=%q", scenario.Name)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func TestPayoutSimulatorResolveByPAN_KnownCard(t *testing.T) {
|
||||
func TestPayoutSimulatorResolveByPAN_Default(t *testing.T) {
|
||||
sim := newPayoutSimulator()
|
||||
|
||||
scenario := sim.resolveByPAN("2200009999999999")
|
||||
scenario := sim.resolveByPAN(defaultScenarioPAN())
|
||||
if scenario.Name != "default_processing" {
|
||||
t.Fatalf("unexpected default scenario: got=%q", scenario.Name)
|
||||
}
|
||||
@@ -31,6 +31,21 @@ func TestPayoutSimulatorResolveByPAN_Default(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayoutSimulatorScenarioPANs_AreLuhnValidAndExpanded(t *testing.T) {
|
||||
sim := newPayoutSimulator()
|
||||
|
||||
for _, scenario := range sim.scenarios {
|
||||
if got, want := len(scenario.CardNumbers), scenarioPANVariants; got != want {
|
||||
t.Fatalf("expected %d PANs for scenario %q, got=%d", want, scenario.Name, got)
|
||||
}
|
||||
for _, pan := range scenario.CardNumbers {
|
||||
if !isValidPANLuhn(pan) {
|
||||
t.Fatalf("scenario %q has invalid Luhn PAN %q", scenario.Name, pan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCardPayoutSendResult_AcceptedSuccessStatus(t *testing.T) {
|
||||
state := &model.CardPayout{}
|
||||
result := &provider.CardPayoutSendResult{
|
||||
|
||||
@@ -155,16 +155,16 @@ 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.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
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/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.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pm "github.com/tech/sendico/pkg/model"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -68,6 +69,8 @@ type StepExecution struct {
|
||||
FailureCode string `bson:"failureCode,omitempty" json:"failureCode,omitempty"`
|
||||
FailureMsg string `bson:"failureMsg,omitempty" json:"failureMsg,omitempty"`
|
||||
ExternalRefs []ExternalRef `bson:"externalRefs,omitempty" json:"externalRefs,omitempty"`
|
||||
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executedMoney,omitempty"`
|
||||
ConvertedMoney *paymenttypes.Money `bson:"convertedMoney,omitempty" json:"convertedMoney,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalRef links step execution to an external operation.
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
type normalizedEvent struct {
|
||||
@@ -15,6 +16,7 @@ type normalizedEvent struct {
|
||||
targetState agg.StepState `bson:"targetState"`
|
||||
failureInfo *failureInfo `bson:"failure,omitempty"`
|
||||
forceAggregate *forceAggregate `bson:"forceAggregate,omitempty"`
|
||||
executedMoney *paymenttypes.Money
|
||||
}
|
||||
|
||||
type failureInfo struct {
|
||||
@@ -123,6 +125,7 @@ func normalizeGatewayEvent(src GatewayEvent) (*normalizedEvent, error) {
|
||||
targetState: target,
|
||||
failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)),
|
||||
forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
|
||||
executedMoney: normalizeEventMoney(src.ExecutedMoney),
|
||||
}
|
||||
ev.matchRefs = normalizeRefList([]agg.ExternalRef{
|
||||
{
|
||||
@@ -264,6 +267,21 @@ func normalizeCardStatus(status CardStatus) (CardStatus, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeEventMoney(money *paymenttypes.Money) *paymenttypes.Money {
|
||||
if money == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(money.GetAmount())
|
||||
currency := strings.TrimSpace(money.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func mapFailureTarget(status any, retryable *bool) (agg.StepState, bool) {
|
||||
switch status {
|
||||
case GatewayStatusCreated, GatewayStatusProcessing, GatewayStatusWaiting:
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -81,6 +82,7 @@ type GatewayEvent struct {
|
||||
TransferRef string
|
||||
GatewayInstanceID string
|
||||
Status GatewayStatus
|
||||
ExecutedMoney *paymenttypes.Money
|
||||
FailureCode string
|
||||
FailureMsg string
|
||||
Retryable *bool
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xerr"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -124,6 +125,7 @@ func (s *svc) applyStepEvent(step *agg.StepExecution, event *normalizedEvent, sm
|
||||
|
||||
if out.State == target {
|
||||
changed = s.applyStepDiagnostics(&out, event) || changed
|
||||
changed = applyExecutedMoney(&out, event) || changed
|
||||
*step = out
|
||||
return changed, nil
|
||||
}
|
||||
@@ -140,11 +142,43 @@ func (s *svc) applyStepEvent(step *agg.StepExecution, event *normalizedEvent, sm
|
||||
out = next
|
||||
changed = changed || transitionChanged
|
||||
changed = s.applyStepDiagnostics(&out, event) || changed
|
||||
changed = applyExecutedMoney(&out, event) || changed
|
||||
|
||||
*step = out
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func applyExecutedMoney(step *agg.StepExecution, event *normalizedEvent) bool {
|
||||
if step == nil || event == nil {
|
||||
return false
|
||||
}
|
||||
money := normalizeEventMoney(event.executedMoney)
|
||||
if money == nil {
|
||||
return false
|
||||
}
|
||||
if normalizeEventMoney(step.ExecutedMoney) != nil {
|
||||
return false
|
||||
}
|
||||
if stepMoneyEqual(step.ExecutedMoney, money) {
|
||||
return false
|
||||
}
|
||||
step.ExecutedMoney = money
|
||||
return true
|
||||
}
|
||||
|
||||
func stepMoneyEqual(left, right *paymenttypes.Money) bool {
|
||||
left = normalizeEventMoney(left)
|
||||
right = normalizeEventMoney(right)
|
||||
switch {
|
||||
case left == nil && right == nil:
|
||||
return true
|
||||
case left == nil || right == nil:
|
||||
return false
|
||||
default:
|
||||
return left.Amount == right.Amount && left.Currency == right.Currency
|
||||
}
|
||||
}
|
||||
|
||||
func transitionStepState(step agg.StepExecution, target agg.StepState, sm ostate.StateMachine) (agg.StepExecution, bool, error) {
|
||||
if step.State == target {
|
||||
return step, false, nil
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -32,6 +33,7 @@ func TestReconcile_GatewayWaiting_UpdatesRunningAndRefs(t *testing.T) {
|
||||
TransferRef: "tx-1",
|
||||
GatewayInstanceID: "gw-1",
|
||||
Status: GatewayStatusWaiting,
|
||||
ExecutedMoney: &paymenttypes.Money{Amount: "5.84", Currency: "USDT"},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -61,6 +63,15 @@ func TestReconcile_GatewayWaiting_UpdatesRunningAndRefs(t *testing.T) {
|
||||
if !hasRef(got.ExternalRefs, agg.ExternalRef{GatewayInstanceID: "gw-1", Kind: ExternalRefKindTransfer, Ref: "tx-1"}) {
|
||||
t.Fatalf("expected transfer_ref external reference")
|
||||
}
|
||||
if got.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money to be mapped")
|
||||
}
|
||||
if gotAmt, want := got.ExecutedMoney.Amount, "5.84"; gotAmt != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", gotAmt, want)
|
||||
}
|
||||
if gotCur, want := got.ExecutedMoney.Currency, "USDT"; gotCur != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", gotCur, want)
|
||||
}
|
||||
if out.Payment.State != agg.StateExecuting {
|
||||
t.Fatalf("aggregate state mismatch: got=%q want=%q", out.Payment.State, agg.StateExecuting)
|
||||
}
|
||||
@@ -109,6 +120,54 @@ func TestReconcile_GatewaySuccess_SettlesPayment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile_GatewayExecutedMoney_DoesNotOverrideExisting(t *testing.T) {
|
||||
now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
|
||||
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})
|
||||
|
||||
out, err := reconciler.Reconcile(Input{
|
||||
Payment: &agg.Payment{
|
||||
PaymentRef: "p1",
|
||||
State: agg.StateCreated,
|
||||
Version: 1,
|
||||
StepExecutions: []agg.StepExecution{
|
||||
{
|
||||
StepRef: "s1",
|
||||
StepCode: "observe",
|
||||
State: agg.StepStatePending,
|
||||
Attempt: 1,
|
||||
ExecutedMoney: &paymenttypes.Money{
|
||||
Amount: "76.50",
|
||||
Currency: "RUB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Event: Event{
|
||||
Gateway: &GatewayEvent{
|
||||
StepRef: "s1",
|
||||
Status: GatewayStatusSuccess,
|
||||
ExecutedMoney: &paymenttypes.Money{
|
||||
Amount: "7650",
|
||||
Currency: "RUB",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
step := out.Payment.StepExecutions[0]
|
||||
if step.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money")
|
||||
}
|
||||
if got, want := step.ExecutedMoney.Amount, "76.50"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := step.ExecutedMoney.Currency, "RUB"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile_GatewayFailureMapping(t *testing.T) {
|
||||
now := time.Date(2026, time.January, 10, 11, 12, 13, 0, time.UTC)
|
||||
reconciler := New(Dependencies{Logger: zap.NewNop(), Now: func() time.Time { return now }})
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
pm "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
@@ -108,6 +109,8 @@ func cloneStepExecutions(src []agg.StepExecution) []agg.StepExecution {
|
||||
step.Attempt = 1
|
||||
}
|
||||
step.ExternalRefs = cloneExternalRefs(step.ExternalRefs)
|
||||
step.ExecutedMoney = cloneStepMoney(step.ExecutedMoney)
|
||||
step.ConvertedMoney = cloneStepMoney(step.ConvertedMoney)
|
||||
step.StartedAt = cloneTime(step.StartedAt)
|
||||
step.CompletedAt = cloneTime(step.CompletedAt)
|
||||
out = append(out, step)
|
||||
@@ -115,6 +118,21 @@ func cloneStepExecutions(src []agg.StepExecution) []agg.StepExecution {
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneStepMoney(money *paymenttypes.Money) *paymenttypes.Money {
|
||||
if money == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(money.GetAmount())
|
||||
currency := strings.TrimSpace(money.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef {
|
||||
if len(refs) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -118,6 +118,24 @@ func TestMap_Success(t *testing.T) {
|
||||
if got, want := steps[0].GetUserLabel(), "Card payout"; got != want {
|
||||
t.Fatalf("user_label mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if steps[0].GetExecutedMoney() == nil {
|
||||
t.Fatal("expected executed_money to be mapped")
|
||||
}
|
||||
if got, want := steps[0].GetExecutedMoney().GetAmount(), "95"; got != want {
|
||||
t.Fatalf("executed_money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].GetExecutedMoney().GetCurrency(), "USD"; got != want {
|
||||
t.Fatalf("executed_money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if steps[0].GetConvertedMoney() == nil {
|
||||
t.Fatal("expected converted_money to be mapped")
|
||||
}
|
||||
if got, want := steps[0].GetConvertedMoney().GetAmount(), "90"; got != want {
|
||||
t.Fatalf("converted_money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].GetConvertedMoney().GetCurrency(), "EUR"; got != want {
|
||||
t.Fatalf("converted_money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[1].GetReportVisibility(), orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN; got != want {
|
||||
t.Fatalf("report_visibility mismatch: got=%s want=%s", got.String(), want.String())
|
||||
}
|
||||
@@ -363,7 +381,15 @@ func newPaymentFixture() *agg.Payment {
|
||||
UserLabel: " Card payout ",
|
||||
State: agg.StepStateRunning,
|
||||
Attempt: 0,
|
||||
StartedAt: &startedAt,
|
||||
ExecutedMoney: &paymenttypes.Money{
|
||||
Amount: "95",
|
||||
Currency: "USD",
|
||||
},
|
||||
ConvertedMoney: &paymenttypes.Money{
|
||||
Amount: "90",
|
||||
Currency: "EUR",
|
||||
},
|
||||
StartedAt: &startedAt,
|
||||
},
|
||||
{
|
||||
StepRef: "s2",
|
||||
|
||||
@@ -46,6 +46,8 @@ func mapStepExecution(step agg.StepExecution, index int) (*orchestrationv2.StepE
|
||||
Refs: mapExternalRefs(step.StepCode, step.ExternalRefs),
|
||||
ReportVisibility: mapReportVisibility(step.ReportVisibility),
|
||||
UserLabel: strings.TrimSpace(step.UserLabel),
|
||||
ExecutedMoney: moneyToProto(step.ExecutedMoney),
|
||||
ConvertedMoney: moneyToProto(step.ConvertedMoney),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ func (defaultObserveConfirmExecutor) ExecuteObserveConfirm(_ context.Context, re
|
||||
step := req.StepExecution
|
||||
step.State = agg.StepStateRunning
|
||||
step.ExternalRefs = refs
|
||||
step.ExecutedMoney = inheritedExecutedMoney(req.Payment, req.Step, req.StepExecution)
|
||||
step.ConvertedMoney = inheritedConvertedMoney(req.Payment, req.Step, req.StepExecution)
|
||||
step.FailureCode = ""
|
||||
step.FailureMsg = ""
|
||||
return &sexec.ExecuteOutput{
|
||||
@@ -198,6 +200,46 @@ func inheritedExternalRefs(payment *agg.Payment, step xplan.Step, current agg.St
|
||||
return refs
|
||||
}
|
||||
|
||||
func inheritedExecutedMoney(payment *agg.Payment, step xplan.Step, current agg.StepExecution) *paymenttypes.Money {
|
||||
if money := cloneStepMoney(current.ExecutedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
if payment == nil || len(step.DependsOn) == 0 {
|
||||
return nil
|
||||
}
|
||||
index := stepIndexByRef(payment.StepExecutions)
|
||||
for i := range step.DependsOn {
|
||||
idx, ok := index[strings.TrimSpace(step.DependsOn[i])]
|
||||
if !ok || idx < 0 || idx >= len(payment.StepExecutions) {
|
||||
continue
|
||||
}
|
||||
if money := cloneStepMoney(payment.StepExecutions[idx].ExecutedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inheritedConvertedMoney(payment *agg.Payment, step xplan.Step, current agg.StepExecution) *paymenttypes.Money {
|
||||
if money := cloneStepMoney(current.ConvertedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
if payment == nil || len(step.DependsOn) == 0 {
|
||||
return nil
|
||||
}
|
||||
index := stepIndexByRef(payment.StepExecutions)
|
||||
for i := range step.DependsOn {
|
||||
idx, ok := index[strings.TrimSpace(step.DependsOn[i])]
|
||||
if !ok || idx < 0 || idx >= len(payment.StepExecutions) {
|
||||
continue
|
||||
}
|
||||
if money := cloneStepMoney(payment.StepExecutions[idx].ConvertedMoney); money != nil {
|
||||
return money
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendExternalRefs(existing []agg.ExternalRef, additions ...agg.ExternalRef) []agg.ExternalRef {
|
||||
out := append([]agg.ExternalRef{}, existing...)
|
||||
seen := map[string]struct{}{}
|
||||
|
||||
@@ -122,6 +122,14 @@ func TestDefaultObserveConfirmExecutor_InheritsDependencyRefs(t *testing.T) {
|
||||
StepExecutions: []agg.StepExecution{
|
||||
{
|
||||
StepRef: "hop_1_crypto_send",
|
||||
ExecutedMoney: &paymenttypes.Money{
|
||||
Amount: "1.000000",
|
||||
Currency: "USDT",
|
||||
},
|
||||
ConvertedMoney: &paymenttypes.Money{
|
||||
Amount: "95.00",
|
||||
Currency: "EUR",
|
||||
},
|
||||
ExternalRefs: []agg.ExternalRef{
|
||||
{GatewayInstanceID: "crypto-gw", Kind: "operation_ref", Ref: "op-1"},
|
||||
{GatewayInstanceID: "crypto-gw", Kind: "transfer_ref", Ref: "trf-1"},
|
||||
@@ -161,4 +169,22 @@ func TestDefaultObserveConfirmExecutor_InheritsDependencyRefs(t *testing.T) {
|
||||
if got, want := out.StepExecution.ExternalRefs[0].Ref, "op-1"; got != want {
|
||||
t.Fatalf("first external ref value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if out.StepExecution.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money to be inherited from dependency")
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Amount, "1.000000"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Currency, "USDT"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if out.StepExecution.ConvertedMoney == nil {
|
||||
t.Fatal("expected converted money to be inherited from dependency")
|
||||
}
|
||||
if got, want := out.StepExecution.ConvertedMoney.Amount, "95.00"; got != want {
|
||||
t.Fatalf("converted money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ConvertedMoney.Currency, "EUR"; got != want {
|
||||
t.Fatalf("converted money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +325,8 @@ func normalizeExecutorOutput(current agg.StepExecution, out *sexec.ExecuteOutput
|
||||
next.Attempt = out.StepExecution.Attempt
|
||||
}
|
||||
next.ExternalRefs = out.StepExecution.ExternalRefs
|
||||
next.ExecutedMoney = cloneStepMoney(out.StepExecution.ExecutedMoney)
|
||||
next.ConvertedMoney = cloneStepMoney(out.StepExecution.ConvertedMoney)
|
||||
next.FailureCode = strings.TrimSpace(out.StepExecution.FailureCode)
|
||||
next.FailureMsg = strings.TrimSpace(out.StepExecution.FailureMsg)
|
||||
|
||||
@@ -437,6 +439,12 @@ func stepExecutionEqual(left, right agg.StepExecution) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !stepMoneyEqual(left.ExecutedMoney, right.ExecutedMoney) {
|
||||
return false
|
||||
}
|
||||
if !stepMoneyEqual(left.ConvertedMoney, right.ConvertedMoney) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package psvc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func cloneStepMoney(src *paymenttypes.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func stepMoneyEqual(left, right *paymenttypes.Money) bool {
|
||||
left = cloneStepMoney(left)
|
||||
right = cloneStepMoney(right)
|
||||
switch {
|
||||
case left == nil && right == nil:
|
||||
return true
|
||||
case left == nil || right == nil:
|
||||
return false
|
||||
default:
|
||||
return left.Amount == right.Amount && left.Currency == right.Currency
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
)
|
||||
|
||||
func (s *svc) prepareInput(in Input) (*preparedInput, error) {
|
||||
@@ -174,6 +175,8 @@ func (s *svc) normalizeStepExecution(exec agg.StepExecution, index int) (agg.Ste
|
||||
exec.Rail = model.ParseRail(string(exec.Rail))
|
||||
exec.ReportVisibility = model.NormalizeReportVisibility(exec.ReportVisibility)
|
||||
exec.ExternalRefs = cloneExternalRefs(exec.ExternalRefs)
|
||||
exec.ExecutedMoney = cloneStepMoney(exec.ExecutedMoney)
|
||||
exec.ConvertedMoney = cloneStepMoney(exec.ConvertedMoney)
|
||||
if exec.StepRef == "" {
|
||||
return agg.StepExecution{}, merrors.InvalidArgument("stepExecutions[" + itoa(index) + "].step_ref is required")
|
||||
}
|
||||
@@ -257,9 +260,26 @@ func normalizeStepState(state agg.StepState) (agg.StepState, bool) {
|
||||
func cloneStepExecution(exec agg.StepExecution) agg.StepExecution {
|
||||
out := exec
|
||||
out.ExternalRefs = cloneExternalRefs(exec.ExternalRefs)
|
||||
out.ExecutedMoney = cloneStepMoney(exec.ExecutedMoney)
|
||||
out.ConvertedMoney = cloneStepMoney(exec.ConvertedMoney)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneStepMoney(money *paymenttypes.Money) *paymenttypes.Money {
|
||||
if money == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(money.GetAmount())
|
||||
currency := strings.TrimSpace(money.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef {
|
||||
if len(refs) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -170,6 +170,11 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
||||
step := req.StepExecution
|
||||
step.State = agg.StepStateCompleted
|
||||
step.ExternalRefs = externalRefs
|
||||
step.ExecutedMoney = cardMinorToMoney(responsePayout.GetAmountMinor(), responsePayout.GetCurrency())
|
||||
if step.ExecutedMoney == nil {
|
||||
step.ExecutedMoney = cardMinorToMoney(amountMinor, currency)
|
||||
}
|
||||
step.ConvertedMoney = nil
|
||||
step.FailureCode = ""
|
||||
step.FailureMsg = ""
|
||||
return &sexec.ExecuteOutput{StepExecution: step}, nil
|
||||
|
||||
@@ -100,9 +100,10 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
|
||||
InstanceID: paymenttypes.DefaultCardsGatewayID,
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "hop_4_card_payout_send",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
Attempt: 1,
|
||||
StepRef: "hop_4_card_payout_send",
|
||||
StepCode: "hop.4.card_payout.send",
|
||||
Attempt: 1,
|
||||
ConvertedMoney: &paymenttypes.Money{Amount: "1.00", Currency: "USD"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -116,6 +117,18 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
|
||||
if out.StepExecution.State != agg.StepStateCompleted {
|
||||
t.Fatalf("expected completed state, got=%q", out.StepExecution.State)
|
||||
}
|
||||
if out.StepExecution.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money to be recorded")
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Amount, "76.50"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Currency, "RUB"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := out.StepExecution.ConvertedMoney; got != nil {
|
||||
t.Fatalf("expected converted money to be cleared for card payout step, got=%+v", got)
|
||||
}
|
||||
if payoutReq == nil {
|
||||
t.Fatal("expected payout request to be submitted")
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ func (e *gatewayCryptoExecutor) ExecuteCrypto(ctx context.Context, req sexec.Ste
|
||||
return nil, refsErr
|
||||
}
|
||||
step.ExternalRefs = refs
|
||||
step.ExecutedMoney = transferExecutedMoney(resp.GetTransfer())
|
||||
if step.ExecutedMoney == nil {
|
||||
step.ExecutedMoney = protoMoneyToPaymentMoney(amount)
|
||||
}
|
||||
step.ConvertedMoney = nil
|
||||
|
||||
if action == discovery.RailOperationSend {
|
||||
if err := e.submitWalletFeeTransfer(ctx, req, client, gateway, sourceWalletRef, operationRef, idempotencyKey); err != nil {
|
||||
|
||||
@@ -106,6 +106,15 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsTransfer(t *testing.T) {
|
||||
if out.StepExecution.State != agg.StepStateCompleted {
|
||||
t.Fatalf("expected completed state, got=%q", out.StepExecution.State)
|
||||
}
|
||||
if out.StepExecution.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money to be recorded")
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Amount, "1.000000"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Currency, "USDT"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if submitReq == nil {
|
||||
t.Fatal("expected transfer submission request")
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ func buildGatewayExecutionEvent(payment *agg.Payment, msg *pmodel.PaymentGateway
|
||||
TransferRef: transferRef,
|
||||
GatewayInstanceID: gatewayInstanceID,
|
||||
Status: status,
|
||||
ExecutedMoney: clonePaymentMoney(msg.ExecutedMoney),
|
||||
}
|
||||
|
||||
switch status {
|
||||
@@ -479,6 +480,7 @@ func (s *Service) pollObserveCandidate(ctx context.Context, payment *agg.Payment
|
||||
TransferRef: strings.TrimSpace(candidate.transferRef),
|
||||
GatewayInstanceID: resolvedObserveGatewayID(candidate.gatewayInstanceID, gateway),
|
||||
Status: status,
|
||||
ExecutedMoney: transferExecutedMoney(transfer),
|
||||
}
|
||||
switch status {
|
||||
case erecon.GatewayStatusFailed:
|
||||
@@ -544,6 +546,10 @@ func (s *Service) resolveObserveGateway(ctx context.Context, payment *agg.Paymen
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.Debug("Gateway for polling not found", zap.String("instance_id", hint.instanceID),
|
||||
zap.String("expected_rail", string(expectedRail)), zap.String("hint_rail", string(hint.rail)))
|
||||
}
|
||||
return nil, errors.New("observe polling: gateway instance not found")
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
pm "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/payments/rail"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -43,9 +44,10 @@ func TestBuildGatewayExecutionEvent_MapsStatusAndMatchedStep(t *testing.T) {
|
||||
}
|
||||
|
||||
event, ok := buildGatewayExecutionEvent(payment, &pm.PaymentGatewayExecution{
|
||||
PaymentRef: payment.PaymentRef,
|
||||
Status: rail.OperationResultSuccess,
|
||||
TransferRef: "trf-1",
|
||||
PaymentRef: payment.PaymentRef,
|
||||
Status: rail.OperationResultSuccess,
|
||||
TransferRef: "trf-1",
|
||||
ExecutedMoney: &paymenttypes.Money{Amount: "5.84", Currency: "USDT"},
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("expected gateway execution event to be accepted")
|
||||
@@ -59,6 +61,15 @@ func TestBuildGatewayExecutionEvent_MapsStatusAndMatchedStep(t *testing.T) {
|
||||
if got, want := event.Status, erecon.GatewayStatusSuccess; got != want {
|
||||
t.Fatalf("status mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if event.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money to be mapped")
|
||||
}
|
||||
if got, want := event.ExecutedMoney.Amount, "5.84"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := event.ExecutedMoney.Currency, "USDT"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGatewayExecutionEvent_FailedSetsTerminalNeedsAttentionHint(t *testing.T) {
|
||||
@@ -602,6 +613,7 @@ func TestPollObserveCandidate_UsesResolvedGatewayAfterInstanceRotation(t *testin
|
||||
TransferRef: req.GetTransferRef(),
|
||||
OperationRef: operationRef,
|
||||
Status: chainv1.TransferStatus_TRANSFER_SUCCESS,
|
||||
NetAmount: &moneyv1.Money{Amount: "460.00", Currency: "RUB"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
@@ -676,6 +688,15 @@ func TestPollObserveCandidate_UsesResolvedGatewayAfterInstanceRotation(t *testin
|
||||
if got, want := gw.GatewayInstanceID, "ea2600ce-3de6-4cc5-bd1e-e26ebaceb6b4"; got != want {
|
||||
t.Fatalf("gateway_instance_id mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if gw.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money in reconciled gateway event")
|
||||
}
|
||||
if got, want := gw.ExecutedMoney.Amount, "460.00"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := gw.ExecutedMoney.Currency, "RUB"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
var _ prepo.Repository = (*fakeExternalRuntimeRepo)(nil)
|
||||
|
||||
@@ -118,6 +118,8 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
step.State = agg.StepStateCompleted
|
||||
step.FailureCode = ""
|
||||
step.FailureMsg = ""
|
||||
step.ExecutedMoney = protoMoneyToPaymentMoney(amount)
|
||||
step.ConvertedMoney = nil
|
||||
step.ExternalRefs = appendLedgerExternalRef(step.ExternalRefs, agg.ExternalRef{
|
||||
GatewayInstanceID: firstNonEmpty(strings.TrimSpace(req.Step.InstanceID), strings.TrimSpace(req.Step.Gateway)),
|
||||
Kind: erecon.ExternalRefKindLedger,
|
||||
|
||||
@@ -42,9 +42,10 @@ func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRol
|
||||
Rail: discovery.RailLedger,
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Attempt: 1,
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Attempt: 1,
|
||||
ConvertedMoney: &paymenttypes.Money{Amount: "42", Currency: "EUR"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -71,6 +72,18 @@ func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRol
|
||||
if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
|
||||
t.Fatalf("state mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if out.StepExecution.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money to be recorded")
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Amount, "1.000000"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Currency, "USDT"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := out.StepExecution.ConvertedMoney; got != nil {
|
||||
t.Fatalf("expected converted money to be cleared for ledger step, got=%+v", got)
|
||||
}
|
||||
if len(out.StepExecution.ExternalRefs) != 1 {
|
||||
t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
)
|
||||
@@ -55,6 +56,10 @@ func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx contex
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
convertedAmount := settlementConvertedMoney(req.Payment)
|
||||
if convertedAmount == nil {
|
||||
return nil, merrors.InvalidArgument("settlement fx_convert: converted amount is required")
|
||||
}
|
||||
|
||||
stepRef := strings.TrimSpace(req.Step.StepRef)
|
||||
operationRef := strings.TrimSpace(req.Payment.PaymentRef) + ":" + stepRef
|
||||
@@ -93,12 +98,21 @@ func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx contex
|
||||
return nil, refsErr
|
||||
}
|
||||
step.ExternalRefs = refs
|
||||
step.ExecutedMoney = protoMoneyToPaymentMoney(amount)
|
||||
step.ConvertedMoney = convertedAmount
|
||||
step.State = agg.StepStateCompleted
|
||||
step.FailureCode = ""
|
||||
step.FailureMsg = ""
|
||||
return &sexec.ExecuteOutput{StepExecution: step}, nil
|
||||
}
|
||||
|
||||
func settlementConvertedMoney(payment *agg.Payment) *paymenttypes.Money {
|
||||
if payment == nil || payment.QuoteSnapshot == nil {
|
||||
return nil
|
||||
}
|
||||
return clonePaymentMoney(payment.QuoteSnapshot.ExpectedSettlementAmount)
|
||||
}
|
||||
|
||||
func (e *gatewayProviderSettlementExecutor) resolveGateway(ctx context.Context, step xplan.Step) (*model.GatewayInstanceDescriptor, error) {
|
||||
if e.gatewayRegistry == nil {
|
||||
return nil, merrors.InvalidArgument("settlement fx_convert: gateway registry is required")
|
||||
|
||||
@@ -101,6 +101,24 @@ func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_SubmitsTran
|
||||
if out.StepExecution.State != agg.StepStateCompleted {
|
||||
t.Fatalf("expected completed state, got=%q", out.StepExecution.State)
|
||||
}
|
||||
if out.StepExecution.ExecutedMoney == nil {
|
||||
t.Fatal("expected executed money to be recorded")
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Amount, "1.000000"; got != want {
|
||||
t.Fatalf("executed money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ExecutedMoney.Currency, "USDT"; got != want {
|
||||
t.Fatalf("executed money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if out.StepExecution.ConvertedMoney == nil {
|
||||
t.Fatal("expected converted money to be recorded for settlement fx_convert")
|
||||
}
|
||||
if got, want := out.StepExecution.ConvertedMoney.Amount, "76.63"; got != want {
|
||||
t.Fatalf("converted money amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.ConvertedMoney.Currency, "RUB"; got != want {
|
||||
t.Fatalf("converted money currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if submitReq == nil {
|
||||
t.Fatal("expected transfer submission request")
|
||||
}
|
||||
@@ -190,3 +208,64 @@ func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_MissingSett
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_MissingConvertedAmount(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
|
||||
executor := &gatewayProviderSettlementExecutor{
|
||||
gatewayInvokeResolver: &fakeGatewayInvokeResolver{
|
||||
client: &chainclient.Fake{},
|
||||
},
|
||||
gatewayRegistry: &fakeGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: "payment_gateway_settlement",
|
||||
InstanceID: "payment_gateway_settlement",
|
||||
Rail: discovery.RailProviderSettlement,
|
||||
InvokeURI: "grpc://tgsettle-gateway",
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := executor.ExecuteProviderSettlement(context.Background(), sexec.StepRequest{
|
||||
Payment: &agg.Payment{
|
||||
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
|
||||
PaymentRef: "payment-3",
|
||||
IdempotencyKey: "idem-3",
|
||||
IntentSnapshot: model.PaymentIntent{
|
||||
Ref: "intent-3",
|
||||
Source: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeManagedWallet,
|
||||
ManagedWallet: &model.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: "wallet-src",
|
||||
},
|
||||
},
|
||||
Amount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
|
||||
},
|
||||
QuoteSnapshot: &model.PaymentQuoteSnapshot{
|
||||
DebitAmount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
|
||||
},
|
||||
},
|
||||
Step: xplan.Step{
|
||||
StepRef: "hop_2_settlement_fx_convert",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
Action: discovery.RailOperationFXConvert,
|
||||
Rail: discovery.RailProviderSettlement,
|
||||
Gateway: "payment_gateway_settlement",
|
||||
InstanceID: "payment_gateway_settlement",
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "hop_2_settlement_fx_convert",
|
||||
StepCode: "hop.2.settlement.fx_convert",
|
||||
Attempt: 1,
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "converted amount is required") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
)
|
||||
|
||||
var cardMinorUnitScale = decimal.NewFromInt(100)
|
||||
|
||||
func clonePaymentMoney(src *paymenttypes.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func protoMoneyToPaymentMoney(src *moneyv1.Money) *paymenttypes.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
if amount == "" || currency == "" {
|
||||
return nil
|
||||
}
|
||||
return &paymenttypes.Money{Amount: amount, Currency: currency}
|
||||
}
|
||||
|
||||
func transferExecutedMoney(transfer *chainv1.Transfer) *paymenttypes.Money {
|
||||
if transfer == nil {
|
||||
return nil
|
||||
}
|
||||
if money := protoMoneyToPaymentMoney(transfer.GetNetAmount()); money != nil {
|
||||
return money
|
||||
}
|
||||
return protoMoneyToPaymentMoney(transfer.GetRequestedAmount())
|
||||
}
|
||||
|
||||
func cardMinorToMoney(amountMinor int64, currency string) *paymenttypes.Money {
|
||||
currency = strings.ToUpper(strings.TrimSpace(currency))
|
||||
if idx := strings.Index(currency, "-"); idx > 0 {
|
||||
currency = currency[:idx]
|
||||
}
|
||||
if currency == "" {
|
||||
return nil
|
||||
}
|
||||
amount := decimal.NewFromInt(amountMinor).Div(cardMinorUnitScale)
|
||||
return &paymenttypes.Money{
|
||||
Amount: amount.StringFixed(2),
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
@@ -155,16 +155,16 @@ 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.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
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/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.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
|
||||
@@ -6,6 +6,7 @@ option go_package = "github.com/tech/sendico/pkg/proto/payments/orchestration/v2
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "api/proto/common/gateway/v1/gateway.proto";
|
||||
import "api/proto/common/money/v1/money.proto";
|
||||
import "api/proto/common/pagination/v1/cursor.proto";
|
||||
import "api/proto/payments/shared/v1/shared.proto";
|
||||
import "api/proto/payments/quotation/v2/quotation.proto";
|
||||
@@ -175,6 +176,10 @@ message StepExecution {
|
||||
ReportVisibility report_visibility = 9;
|
||||
// Optional user-facing operation label.
|
||||
string user_label = 10;
|
||||
// Final amount processed by this step (if known).
|
||||
common.money.v1.Money executed_money = 11;
|
||||
// Converted amount produced by this step (for FX operations).
|
||||
common.money.v1.Money converted_money = 12;
|
||||
}
|
||||
|
||||
// Kept local on purpose: no shared enum exists for orchestration step runtime.
|
||||
|
||||
@@ -27,7 +27,7 @@ make up
|
||||
make vault-init
|
||||
|
||||
# 4. View logs
|
||||
make logs SERVICE=ledger
|
||||
make logs SERVICE=dev-ledger
|
||||
|
||||
# 5. Stop everything
|
||||
make down
|
||||
@@ -46,8 +46,8 @@ All in `.env.dev` (plaintext for dev):
|
||||
make build # Build all images
|
||||
make up # Start environment
|
||||
make down # Stop environment
|
||||
make logs SERVICE=ledger # View service logs
|
||||
make rebuild SERVICE=ledger # Rebuild specific service
|
||||
make logs SERVICE=dev-ledger # View service logs
|
||||
make rebuild SERVICE=dev-ledger # Rebuild specific service
|
||||
make proto # Regenerate protobuf
|
||||
make clean # Remove everything
|
||||
make status # Check service status
|
||||
@@ -78,11 +78,11 @@ After code changes:
|
||||
|
||||
```bash
|
||||
# Rebuild specific service
|
||||
make rebuild SERVICE=ledger
|
||||
make rebuild SERVICE=dev-ledger
|
||||
|
||||
# Or manually
|
||||
docker compose -f docker-compose.dev.yml build ledger
|
||||
docker compose -f docker-compose.dev.yml up -d --force-recreate ledger
|
||||
docker compose -f docker-compose.dev.yml build dev-ledger
|
||||
docker compose -f docker-compose.dev.yml up -d --force-recreate dev-ledger
|
||||
```
|
||||
|
||||
## Adding New Services
|
||||
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
sendico_bff:
|
||||
<<: *common-env
|
||||
container_name: sendico-bff
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/bff/service:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_billing_documents:
|
||||
<<: *common-env
|
||||
container_name: sendico-billing-documents
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/billing/documents:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_billing_fees:
|
||||
<<: *common-env
|
||||
container_name: sendico-billing-fees
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/billing/fees:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
sendico_callbacks:
|
||||
<<: *common-env
|
||||
container_name: sendico-callbacks
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/edge/callbacks:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
sendico_chain_gateway:
|
||||
<<: *common-env
|
||||
container_name: sendico-chain-gateway
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/gateway/chain:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_discovery:
|
||||
<<: *common-env
|
||||
container_name: sendico-discovery
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/discovery/service:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_fx_ingestor:
|
||||
<<: *common-env
|
||||
container_name: sendico-fx-ingestor
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/fx/ingestor:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_fx_oracle:
|
||||
<<: *common-env
|
||||
container_name: sendico-fx-oracle
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/fx/oracle:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_ledger:
|
||||
<<: *common-env
|
||||
container_name: sendico-ledger
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/ledger/service:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_mntx_gateway:
|
||||
<<: *common-env
|
||||
container_name: sendico-mntx-gateway
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/gateway/mntx:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_notification:
|
||||
<<: *common-env
|
||||
container_name: sendico-notification
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/notification/service:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_payments_methods:
|
||||
<<: *common-env
|
||||
container_name: sendico-payments-methods
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/payments/methods:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_payments_orchestrator:
|
||||
<<: *common-env
|
||||
container_name: sendico-payments-orchestrator
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/payments/orchestrator:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_payments_quotation:
|
||||
<<: *common-env
|
||||
container_name: sendico-payments-quotation
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/payments/quotation:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -14,6 +14,7 @@ services:
|
||||
sendico_tgsettle_gateway:
|
||||
<<: *common-env
|
||||
container_name: sendico-tgsettle-gateway
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/gateway/tgsettle:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
sendico_tron_gateway:
|
||||
<<: *common-env
|
||||
container_name: sendico-tron-gateway
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
image: ${REGISTRY_URL}/gateway/tron:${APP_V}
|
||||
pull_policy: always
|
||||
|
||||
@@ -3,14 +3,16 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:pshared/data/dto/payment/operation.dart';
|
||||
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
||||
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
||||
import 'package:pshared/data/dto/payment/response_endpoint.dart';
|
||||
|
||||
part 'payment.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentDTO {
|
||||
final String? paymentRef;
|
||||
final String? idempotencyKey;
|
||||
final String? state;
|
||||
final PaymentResponseEndpointDTO? source;
|
||||
final PaymentResponseEndpointDTO? destination;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final PaymentIntentDTO? intent;
|
||||
@@ -21,8 +23,9 @@ class PaymentDTO {
|
||||
|
||||
const PaymentDTO({
|
||||
this.paymentRef,
|
||||
this.idempotencyKey,
|
||||
this.state,
|
||||
this.source,
|
||||
this.destination,
|
||||
this.failureCode,
|
||||
this.failureReason,
|
||||
this.intent,
|
||||
|
||||
22
frontend/pshared/lib/data/dto/payment/response_endpoint.dart
Normal file
22
frontend/pshared/lib/data/dto/payment/response_endpoint.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'response_endpoint.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentResponseEndpointDTO {
|
||||
final String? type;
|
||||
final Map<String, dynamic>? data;
|
||||
final String? paymentMethodRef;
|
||||
final String? payeeRef;
|
||||
|
||||
const PaymentResponseEndpointDTO({
|
||||
this.type,
|
||||
this.data,
|
||||
this.paymentMethodRef,
|
||||
this.payeeRef,
|
||||
});
|
||||
|
||||
factory PaymentResponseEndpointDTO.fromJson(Map<String, dynamic> json) =>
|
||||
_$PaymentResponseEndpointDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PaymentResponseEndpointDTOToJson(this);
|
||||
}
|
||||
@@ -2,20 +2,24 @@ import 'package:pshared/data/dto/payment/card.dart';
|
||||
import 'package:pshared/data/dto/payment/card_token.dart';
|
||||
import 'package:pshared/data/dto/payment/endpoint.dart';
|
||||
import 'package:pshared/data/dto/payment/external_chain.dart';
|
||||
import 'package:pshared/data/dto/payment/iban.dart';
|
||||
import 'package:pshared/data/dto/payment/ledger.dart';
|
||||
import 'package:pshared/data/dto/payment/managed_wallet.dart';
|
||||
import 'package:pshared/data/dto/payment/russian_bank.dart';
|
||||
import 'package:pshared/data/dto/payment/wallet.dart';
|
||||
import 'package:pshared/data/mapper/payment/asset.dart';
|
||||
import 'package:pshared/data/mapper/payment/enums.dart';
|
||||
import 'package:pshared/data/mapper/payment/type.dart';
|
||||
import 'package:pshared/models/payment/methods/card.dart';
|
||||
import 'package:pshared/models/payment/methods/card_token.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/iban.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/russian_bank.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
|
||||
extension PaymentMethodDataEndpointMapper on PaymentMethodData {
|
||||
PaymentEndpointDTO toDTO() {
|
||||
final metadata = this.metadata;
|
||||
@@ -76,8 +80,40 @@ extension PaymentMethodDataEndpointMapper on PaymentMethodData {
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError('Unsupported payment endpoint type: $type');
|
||||
case PaymentType.wallet:
|
||||
final payload = this as WalletPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: endpointTypeToValue(type),
|
||||
data: WalletPaymentDataDTO(walletId: payload.walletId).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.bankAccount:
|
||||
final payload = this as RussianBankAccountPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: endpointTypeToValue(type),
|
||||
data: RussianBankAccountPaymentDataDTO(
|
||||
recipientName: payload.recipientName,
|
||||
inn: payload.inn,
|
||||
kpp: payload.kpp,
|
||||
bankName: payload.bankName,
|
||||
bik: payload.bik,
|
||||
accountNumber: payload.accountNumber,
|
||||
correspondentAccount: payload.correspondentAccount,
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.iban:
|
||||
final payload = this as IbanPaymentMethod;
|
||||
return PaymentEndpointDTO(
|
||||
type: endpointTypeToValue(type),
|
||||
data: IbanPaymentDataDTO(
|
||||
iban: payload.iban,
|
||||
accountHolder: payload.accountHolder,
|
||||
bic: payload.bic,
|
||||
bankName: payload.bankName,
|
||||
).toJson(),
|
||||
metadata: metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,14 +163,40 @@ extension PaymentEndpointDTOMapper on PaymentEndpointDTO {
|
||||
maskedPan: payload.maskedPan,
|
||||
metadata: metadata,
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError('Unsupported payment endpoint type: ${paymentTypeFromValue(type)}');
|
||||
case PaymentType.wallet:
|
||||
final payload = WalletPaymentDataDTO.fromJson(data);
|
||||
return WalletPaymentMethod(
|
||||
walletId: payload.walletId,
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.bankAccount:
|
||||
final payload = RussianBankAccountPaymentDataDTO.fromJson(data);
|
||||
return RussianBankAccountPaymentMethod(
|
||||
recipientName: payload.recipientName,
|
||||
inn: payload.inn,
|
||||
kpp: payload.kpp,
|
||||
bankName: payload.bankName,
|
||||
bik: payload.bik,
|
||||
accountNumber: payload.accountNumber,
|
||||
correspondentAccount: payload.correspondentAccount,
|
||||
metadata: metadata,
|
||||
);
|
||||
case PaymentType.iban:
|
||||
final payload = IbanPaymentDataDTO.fromJson(data);
|
||||
return IbanPaymentMethod(
|
||||
iban: payload.iban,
|
||||
accountHolder: payload.accountHolder,
|
||||
bic: payload.bic,
|
||||
bankName: payload.bankName,
|
||||
metadata: metadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PaymentType _resolveEndpointType(String type, Map<String, dynamic> data) {
|
||||
if (type == 'card' && (data.containsKey('token') || data.containsKey('masked_pan'))) {
|
||||
if (type == 'card' &&
|
||||
(data.containsKey('token') || data.containsKey('masked_pan'))) {
|
||||
return PaymentType.cardToken;
|
||||
}
|
||||
return endpointTypeFromValue(type);
|
||||
|
||||
@@ -2,14 +2,16 @@ import 'package:pshared/data/dto/payment/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/operation.dart';
|
||||
import 'package:pshared/data/mapper/payment/quote.dart';
|
||||
import 'package:pshared/data/mapper/payment/response_endpoint.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
|
||||
extension PaymentDTOMapper on PaymentDTO {
|
||||
Payment toDomain() => Payment(
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state,
|
||||
source: source?.toDomain(),
|
||||
destination: destination?.toDomain(),
|
||||
orchestrationState: paymentOrchestrationStateFromValue(state),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
@@ -24,8 +26,9 @@ extension PaymentDTOMapper on PaymentDTO {
|
||||
extension PaymentMapper on Payment {
|
||||
PaymentDTO toDTO() => PaymentDTO(
|
||||
paymentRef: paymentRef,
|
||||
idempotencyKey: idempotencyKey,
|
||||
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
||||
source: source?.toDTO(),
|
||||
destination: destination?.toDTO(),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
intent: intent?.toDTO(),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:pshared/data/dto/payment/endpoint.dart';
|
||||
import 'package:pshared/data/dto/payment/response_endpoint.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/endpoint.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
|
||||
extension PaymentResponseEndpointDTOMapper on PaymentResponseEndpointDTO {
|
||||
PaymentEndpoint toDomain() {
|
||||
final normalizedType = _normalize(type);
|
||||
final normalizedData = _cloneData(data);
|
||||
|
||||
return PaymentEndpoint(
|
||||
method: _tryParseMethod(normalizedType, normalizedData),
|
||||
paymentMethodRef: _normalize(paymentMethodRef),
|
||||
payeeRef: _normalize(payeeRef),
|
||||
type: normalizedType,
|
||||
rawData: normalizedData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension PaymentEndpointMapper on PaymentEndpoint {
|
||||
PaymentResponseEndpointDTO toDTO() {
|
||||
final methodData = method;
|
||||
if (methodData != null) {
|
||||
final endpoint = methodData.toDTO();
|
||||
return PaymentResponseEndpointDTO(
|
||||
type: endpoint.type,
|
||||
data: endpoint.data,
|
||||
paymentMethodRef: _normalize(paymentMethodRef),
|
||||
payeeRef: _normalize(payeeRef),
|
||||
);
|
||||
}
|
||||
|
||||
return PaymentResponseEndpointDTO(
|
||||
type: _normalize(type),
|
||||
data: _cloneData(rawData),
|
||||
paymentMethodRef: _normalize(paymentMethodRef),
|
||||
payeeRef: _normalize(payeeRef),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PaymentMethodData? _tryParseMethod(String? type, Map<String, dynamic>? data) {
|
||||
if (type == null || data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return PaymentEndpointDTO(type: type, data: data).toDomain();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _normalize(String? value) {
|
||||
final trimmed = value?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _cloneData(Map<String, dynamic>? data) {
|
||||
if (data == null) return null;
|
||||
if (data.isEmpty) return <String, dynamic>{};
|
||||
return Map<String, dynamic>.from(data);
|
||||
}
|
||||
17
frontend/pshared/lib/models/payment/endpoint.dart
Normal file
17
frontend/pshared/lib/models/payment/endpoint.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
|
||||
class PaymentEndpoint {
|
||||
final PaymentMethodData? method;
|
||||
final String? paymentMethodRef;
|
||||
final String? payeeRef;
|
||||
final String? type;
|
||||
final Map<String, dynamic>? rawData;
|
||||
|
||||
const PaymentEndpoint({
|
||||
required this.method,
|
||||
required this.paymentMethodRef,
|
||||
required this.payeeRef,
|
||||
required this.type,
|
||||
required this.rawData,
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:pshared/models/payment/endpoint.dart';
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/quote/quote.dart';
|
||||
@@ -5,8 +6,9 @@ import 'package:pshared/models/payment/state.dart';
|
||||
|
||||
class Payment {
|
||||
final String? paymentRef;
|
||||
final String? idempotencyKey;
|
||||
final String? state;
|
||||
final PaymentEndpoint? source;
|
||||
final PaymentEndpoint? destination;
|
||||
final PaymentOrchestrationState orchestrationState;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
@@ -18,8 +20,9 @@ class Payment {
|
||||
|
||||
const Payment({
|
||||
required this.paymentRef,
|
||||
required this.idempotencyKey,
|
||||
required this.state,
|
||||
required this.source,
|
||||
required this.destination,
|
||||
required this.orchestrationState,
|
||||
required this.failureCode,
|
||||
required this.failureReason,
|
||||
|
||||
@@ -254,9 +254,7 @@ class PaymentsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
String? _paymentKey(Payment payment) {
|
||||
final ref = _normalize(payment.paymentRef);
|
||||
if (ref != null) return ref;
|
||||
return _normalize(payment.idempotencyKey);
|
||||
return _normalize(payment.paymentRef);
|
||||
}
|
||||
|
||||
List<String>? _normalizeStates(List<String>? states) {
|
||||
|
||||
@@ -79,8 +79,6 @@ class PaymentsUpdatesProvider extends ChangeNotifier {
|
||||
String? _key(Payment payment) {
|
||||
final ref = payment.paymentRef?.trim();
|
||||
if (ref != null && ref.isNotEmpty) return ref;
|
||||
final idempotency = payment.idempotencyKey?.trim();
|
||||
if (idempotency != null && idempotency.isNotEmpty) return idempotency;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,9 @@ void main() {
|
||||
test('isPending and isTerminal are derived from typed state', () {
|
||||
const created = Payment(
|
||||
paymentRef: 'p-1',
|
||||
idempotencyKey: 'idem-1',
|
||||
state: 'orchestration_state_created',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.created,
|
||||
failureCode: null,
|
||||
failureReason: null,
|
||||
@@ -76,8 +77,9 @@ void main() {
|
||||
);
|
||||
const settled = Payment(
|
||||
paymentRef: 'p-2',
|
||||
idempotencyKey: 'idem-2',
|
||||
state: 'orchestration_state_settled',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.settled,
|
||||
failureCode: null,
|
||||
failureReason: null,
|
||||
@@ -96,8 +98,9 @@ void main() {
|
||||
test('isFailure handles both explicit code and failed state', () {
|
||||
const withFailureCode = Payment(
|
||||
paymentRef: 'p-3',
|
||||
idempotencyKey: 'idem-3',
|
||||
state: 'orchestration_state_executing',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.executing,
|
||||
failureCode: 'failure_ledger',
|
||||
failureReason: 'ledger failed',
|
||||
@@ -108,8 +111,9 @@ void main() {
|
||||
);
|
||||
const failedState = Payment(
|
||||
paymentRef: 'p-4',
|
||||
idempotencyKey: 'idem-4',
|
||||
state: 'orchestration_state_failed',
|
||||
source: null,
|
||||
destination: null,
|
||||
orchestrationState: PaymentOrchestrationState.failed,
|
||||
failureCode: null,
|
||||
failureReason: null,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:test/test.dart';
|
||||
import 'package:pshared/api/requests/payment/initiate.dart';
|
||||
import 'package:pshared/api/requests/payment/initiate_payments.dart';
|
||||
import 'package:pshared/api/requests/payment/quote.dart';
|
||||
import 'package:pshared/api/responses/payment/payment.dart';
|
||||
import 'package:pshared/api/responses/payment/quotation.dart';
|
||||
import 'package:pshared/data/dto/money.dart';
|
||||
import 'package:pshared/data/dto/payment/currency_pair.dart';
|
||||
@@ -12,11 +13,13 @@ import 'package:pshared/data/dto/payment/endpoint.dart';
|
||||
import 'package:pshared/data/dto/payment/intent/fx.dart';
|
||||
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/payment_response.dart';
|
||||
import 'package:pshared/models/payment/asset.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/methods/card_token.dart';
|
||||
import 'package:pshared/models/payment/methods/crypto_address.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
|
||||
void main() {
|
||||
group('Payment request DTO contract', () {
|
||||
@@ -185,5 +188,38 @@ void main() {
|
||||
expect(json.containsKey('intentRef'), isFalse);
|
||||
expect(json.containsKey('intentRefs'), isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'payment response parses source and destination endpoint snapshots',
|
||||
() {
|
||||
final response = PaymentResponse.fromJson({
|
||||
'accessToken': {
|
||||
'token': 'token',
|
||||
'expiration': '2026-02-25T00:00:00Z',
|
||||
},
|
||||
'payment': {
|
||||
'paymentRef': 'pay-1',
|
||||
'state': 'orchestration_state_created',
|
||||
'source': {
|
||||
'type': 'wallet',
|
||||
'data': {'walletId': 'wallet-1'},
|
||||
},
|
||||
'destination': {'paymentMethodRef': 'pm-123'},
|
||||
'operations': [],
|
||||
'meta': {'quotationRef': 'quote-1'},
|
||||
},
|
||||
});
|
||||
|
||||
final payment = response.payment.toDomain();
|
||||
expect(payment.paymentRef, equals('pay-1'));
|
||||
expect(payment.source, isNotNull);
|
||||
expect(payment.destination, isNotNull);
|
||||
expect(payment.destination?.paymentMethodRef, equals('pm-123'));
|
||||
expect(payment.source?.method, isA<WalletPaymentMethod>());
|
||||
|
||||
final sourceMethod = payment.source?.method as WalletPaymentMethod;
|
||||
expect(sourceMethod.walletId, equals('wallet-1'));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ class PaymentDetailsController extends ChangeNotifier {
|
||||
if (trimmed.isEmpty) return null;
|
||||
for (final payment in payments) {
|
||||
if (payment.paymentRef == trimmed) return payment;
|
||||
if (payment.idempotencyKey == trimmed) return payment;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -20,15 +20,9 @@ OperationItem mapPaymentToOperation(Payment payment) {
|
||||
: parseMoneyAmount(settlement.amount);
|
||||
final toCurrency = settlement?.currency ?? currency;
|
||||
|
||||
final payId =
|
||||
_firstNonEmpty([payment.paymentRef, payment.idempotencyKey]) ?? '-';
|
||||
final payId = _firstNonEmpty([payment.paymentRef]) ?? '-';
|
||||
final name =
|
||||
_firstNonEmpty([
|
||||
payment.lastQuote?.quoteRef,
|
||||
payment.paymentRef,
|
||||
payment.idempotencyKey,
|
||||
]) ??
|
||||
'-';
|
||||
_firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef]) ?? '-';
|
||||
final comment =
|
||||
_firstNonEmpty([
|
||||
payment.failureReason,
|
||||
|
||||
@@ -76,6 +76,10 @@ services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
networks: [cicd]
|
||||
ports:
|
||||
- target: 22
|
||||
published: 2222
|
||||
mode: host
|
||||
depends_on:
|
||||
- gitea-db
|
||||
- vault-agent-gitea
|
||||
|
||||
35
infra/gitea/vault/agent.hcl
Normal file
35
infra/gitea/vault/agent.hcl
Normal file
@@ -0,0 +1,35 @@
|
||||
auto_auth {
|
||||
method "approle" {
|
||||
mount_path = "auth/approle"
|
||||
config = {
|
||||
role_id_file_path = "/vault/secrets/role_id"
|
||||
secret_id_file_path = "/vault/secrets/secret_id"
|
||||
}
|
||||
}
|
||||
sink "file" { config = { path = "/vault/.token" } }
|
||||
}
|
||||
|
||||
template {
|
||||
source = "/etc/vault/templates/gitea_db_pass.ctmpl"
|
||||
destination = "/vault/secrets/gitea_db_pass"
|
||||
}
|
||||
|
||||
template {
|
||||
source = "/etc/vault/templates/minio_access_key.ctmpl"
|
||||
destination = "/vault/secrets/minio_access_key"
|
||||
}
|
||||
|
||||
template {
|
||||
source = "/etc/vault/templates/minio_secret_key.ctmpl"
|
||||
destination = "/vault/secrets/minio_secret_key"
|
||||
}
|
||||
|
||||
template {
|
||||
source = "/etc/vault/templates/mail_account.ctmpl"
|
||||
destination = "/vault/secrets/mail_account"
|
||||
}
|
||||
|
||||
template {
|
||||
source = "/etc/vault/templates/mail_secret.ctmpl"
|
||||
destination = "/vault/secrets/mail_secret"
|
||||
}
|
||||
1
infra/gitea/vault/templates/gitea_db_pass.ctmpl
Normal file
1
infra/gitea/vault/templates/gitea_db_pass.ctmpl
Normal file
@@ -0,0 +1 @@
|
||||
{{ with secret "kv/data/cicd/gitea" }}{{ .Data.data.gitea_db_pass }}{{- end -}}
|
||||
1
infra/gitea/vault/templates/mail_account.ctmpl
Normal file
1
infra/gitea/vault/templates/mail_account.ctmpl
Normal file
@@ -0,0 +1 @@
|
||||
{{ with secret "kv/data/cicd/gitea" }}{{ .Data.data.mail_account }}{{- end -}}
|
||||
1
infra/gitea/vault/templates/mail_secret.ctmpl
Normal file
1
infra/gitea/vault/templates/mail_secret.ctmpl
Normal file
@@ -0,0 +1 @@
|
||||
{{ with secret "kv/data/cicd/gitea" }}{{ .Data.data.mail_secret }}{{- end -}}
|
||||
1
infra/gitea/vault/templates/minio_access_key.ctmpl
Normal file
1
infra/gitea/vault/templates/minio_access_key.ctmpl
Normal file
@@ -0,0 +1 @@
|
||||
{{ with secret "kv/data/s3/gitea" }}{{ .Data.data.access_key_id }}{{- end -}}
|
||||
1
infra/gitea/vault/templates/minio_secret_key.ctmpl
Normal file
1
infra/gitea/vault/templates/minio_secret_key.ctmpl
Normal file
@@ -0,0 +1 @@
|
||||
{{ with secret "kv/data/s3/gitea" }}{{ .Data.data.secret_access_key }}{{- end -}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user