Compare commits

1 Commits

Author SHA1 Message Date
Arseni
f44ef56ff3 WIP: integration with ledger 2026-02-04 02:01:22 +03:00
1384 changed files with 23141 additions and 79759 deletions

View File

@@ -9,17 +9,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/server/**
- api/payments/methods/client/**
- api/payments/methods/go.mod
- api/payments/methods/go.sum
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/bff.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -46,14 +35,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh bff
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -74,7 +55,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/bff/build-image.sh - sh ci/scripts/bff/build-image.sh

View File

@@ -8,14 +8,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/billing/documents/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/billing_documents.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -42,14 +34,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh billing_documents
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -70,7 +54,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/billing_documents/build-image.sh - sh ci/scripts/billing_documents/build-image.sh

View File

@@ -8,14 +8,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/billing/fees/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/billing_fees.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -42,14 +34,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh billing_fees
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -70,7 +54,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/billing_fees/build-image.sh - sh ci/scripts/billing_fees/build-image.sh

View File

@@ -1,9 +1,6 @@
when: when:
- event: push - event: push
branch: main branch: main
path:
exclude: ['**']
ignore_message: '[infra]'
steps: steps:
- name: version - name: version

View File

@@ -7,14 +7,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/discovery/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/discovery.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -41,14 +33,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh discovery
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -69,7 +53,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/discovery/build-image.sh - sh ci/scripts/discovery/build-image.sh

View File

@@ -7,16 +7,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/server/**
- api/pkg/**
- api/proto/**
- frontend/**
- interface/**
- ci/prod/**
- .woodpecker/frontend.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version

View File

@@ -11,15 +11,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/fx/ingestor/**
- api/fx/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/fx_ingestor.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -47,14 +38,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh fx_ingestor
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -75,7 +58,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/fx/build-image.sh - sh ci/scripts/fx/build-image.sh

View File

@@ -11,16 +11,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/fx/oracle/**
- api/fx/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/fx_oracle.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -48,14 +38,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh fx_oracle
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -76,7 +58,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/fx/build-image.sh - sh ci/scripts/fx/build-image.sh

View File

@@ -11,15 +11,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/chain/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_chain.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -46,14 +37,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_chain
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -74,7 +57,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/chain_gateway/build-image.sh - sh ci/scripts/chain_gateway/build-image.sh

View File

@@ -10,15 +10,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/mntx/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_mntx.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -45,14 +36,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_mntx
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -73,7 +56,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/mntx/build-image.sh - sh ci/scripts/mntx/build-image.sh

View File

@@ -8,15 +8,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/tgsettle/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_tgsettle.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -43,14 +34,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_tgsettle
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -71,7 +54,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/tgsettle/build-image.sh - sh ci/scripts/tgsettle/build-image.sh

View File

@@ -11,15 +11,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/tron/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_tron.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -46,14 +37,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_tron
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -74,7 +57,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/tron_gateway/build-image.sh - sh ci/scripts/tron_gateway/build-image.sh

View File

@@ -8,14 +8,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/ledger/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/ledger.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -42,14 +34,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh ledger
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -70,7 +54,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/ledger/build-image.sh - sh ci/scripts/ledger/build-image.sh

View File

@@ -1,10 +1,6 @@
when: when:
- event: push - event: push
branch: main branch: main
path:
exclude: ['**']
ignore_message: '[infra]'
steps: steps:
- name: version - name: version

View File

@@ -11,14 +11,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/notification/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/notification.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -45,14 +37,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh notification
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -73,7 +57,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/notification/build-image.sh - sh ci/scripts/notification/build-image.sh

View File

@@ -1,90 +0,0 @@
matrix:
include:
- PAYMENTS_METHODS_IMAGE_PATH: payments/methods
PAYMENTS_METHODS_DOCKERFILE: ci/prod/compose/payments_methods.dockerfile
PAYMENTS_METHODS_MONGO_SECRET_PATH: sendico/db
PAYMENTS_METHODS_ENV: prod
when:
- event: push
branch: main
path:
include:
- api/payments/methods/**
- api/payments/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/payments_methods.yml
ignore_message: '[rebuild]'
steps:
- name: version
image: alpine:latest
commands:
- set -euo pipefail 2>/dev/null || set -eu
- apk add --no-cache git
- GIT_REV="$(git rev-parse --short HEAD)"
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
- APP_V="$(cat version)"
- BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}"
- printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \
"$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version
- name: proto
image: golang:alpine
depends_on: [ version ]
commands:
- set -eu
- apk add --no-cache bash git build-base protoc protobuf-dev
- go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh payments_methods
- name: secrets
image: alpine:latest
depends_on: [ version ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash coreutils openssh-keygen curl sed python3
- mkdir -p secrets
- ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600
- base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY
- chmod 600 secrets/SSH_KEY
- ssh-keygen -y -f secrets/SSH_KEY >/dev/null
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
commands:
- sh ci/scripts/payments_methods/build-image.sh
- name: deploy
image: alpine:latest
depends_on: [ secrets, build-image ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash openssh-client rsync coreutils curl sed python3
- mkdir -p /root/.ssh
- install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa
- sh ci/scripts/payments_methods/deploy.sh

View File

@@ -8,15 +8,6 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/payments/orchestrator/**
- api/payments/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/payments_orchestrator.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -43,14 +34,6 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh payments_orchestrator
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -71,7 +54,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ proto, secrets ]
commands: commands:
- sh ci/scripts/payments_orchestrator/build-image.sh - sh ci/scripts/payments_orchestrator/build-image.sh

View File

@@ -1,90 +0,0 @@
matrix:
include:
- PAYMENTS_QUOTATION_IMAGE_PATH: payments/quotation
PAYMENTS_QUOTATION_DOCKERFILE: ci/prod/compose/payments_quotation.dockerfile
PAYMENTS_QUOTATION_MONGO_SECRET_PATH: sendico/db
PAYMENTS_QUOTATION_ENV: prod
when:
- event: push
branch: main
path:
include:
- api/payments/quotation/**
- api/payments/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/payments_quotation.yml
ignore_message: '[rebuild]'
steps:
- name: version
image: alpine:latest
commands:
- set -euo pipefail 2>/dev/null || set -eu
- apk add --no-cache git
- GIT_REV="$(git rev-parse --short HEAD)"
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
- APP_V="$(cat version)"
- BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}"
- printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \
"$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version
- name: proto
image: golang:alpine
depends_on: [ version ]
commands:
- set -eu
- apk add --no-cache bash git build-base protoc protobuf-dev
- go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh payments_quotation
- name: secrets
image: alpine:latest
depends_on: [ version ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash coreutils openssh-keygen curl sed python3
- mkdir -p secrets
- ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600
- base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY
- chmod 600 secrets/SSH_KEY
- ssh-keygen -y -f secrets/SSH_KEY >/dev/null
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
commands:
- sh ci/scripts/payments_quotation/build-image.sh
- name: deploy
image: alpine:latest
depends_on: [ secrets, build-image ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash openssh-client rsync coreutils curl sed python3
- mkdir -p /root/.ssh
- install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa
- sh ci/scripts/payments_quotation/deploy.sh

View File

@@ -1,7 +1,7 @@
# Sendico Development Environment - Makefile # Sendico Development Environment - Makefile
# Docker Compose + Makefile build system # Docker Compose + Makefile build system
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend .PHONY: help init build up down restart logs rebuild clean vault-init proto
COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev
SERVICE ?= SERVICE ?=
@@ -43,15 +43,6 @@ help:
@echo "" @echo ""
@echo "$(YELLOW)Development:$(NC)" @echo "$(YELLOW)Development:$(NC)"
@echo " make proto Generate protobuf code" @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-frontend Generate Flutter code only"
@echo " make update Update all dependencies (Go + Flutter)"
@echo " make update-api 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-frontend Run Flutter tests only"
@echo " make health Check service health" @echo " make health Check service health"
@echo "" @echo ""
@echo "Examples:" @echo "Examples:"
@@ -85,7 +76,7 @@ init:
@echo "$(GREEN)Verifying .env.dev...$(NC)" @echo "$(GREEN)Verifying .env.dev...$(NC)"
@cat .env.dev | grep -q "MONGO_USER=" || (echo "$(YELLOW)Error: .env.dev is incomplete$(NC)" && exit 1) @cat .env.dev | grep -q "MONGO_USER=" || (echo "$(YELLOW)Error: .env.dev is incomplete$(NC)" && exit 1)
@echo "$(GREEN)Running proto generation...$(NC)" @echo "$(GREEN)Running proto generation...$(NC)"
@./ci/scripts/proto/generate.sh @./generate_protos.sh
@echo "$(GREEN)Building Docker images...$(NC)" @echo "$(GREEN)Building Docker images...$(NC)"
@$(COMPOSE) build @$(COMPOSE) build
@echo "$(GREEN)✅ Initialization complete!$(NC)" @echo "$(GREEN)✅ Initialization complete!$(NC)"
@@ -97,7 +88,7 @@ init:
# Build all images # Build all images
build: build:
@echo "$(GREEN)Building all service images...$(NC)" @echo "$(GREEN)Building all service images...$(NC)"
@./ci/scripts/proto/generate.sh @./generate_protos.sh
@$(COMPOSE) build @$(COMPOSE) build
# Start all services # Start all services
@@ -145,25 +136,12 @@ endif
@echo "$(GREEN)✅ $(SERVICE) rebuilt$(NC)" @echo "$(GREEN)✅ $(SERVICE) rebuilt$(NC)"
@echo "View logs: make logs SERVICE=$(SERVICE)" @echo "View logs: make logs SERVICE=$(SERVICE)"
# Generate protobuf code (alias)
proto: generate-api
# Generate all code
generate: generate-api generate-frontend
# Generate protobuf code # Generate protobuf code
generate-api: proto:
@echo "$(GREEN)Generating protobuf code...$(NC)" @echo "$(GREEN)Generating protobuf code...$(NC)"
@./ci/scripts/proto/generate.sh @./generate_protos.sh
@echo "$(GREEN)✅ Protobuf generation complete$(NC)" @echo "$(GREEN)✅ Protobuf generation complete$(NC)"
# Generate Flutter code (json_serializable, etc.)
generate-frontend:
@echo "$(GREEN)Generating Flutter code...$(NC)"
@cd frontend/pshared && dart run build_runner build --delete-conflicting-outputs
@cd frontend/pweb && dart run build_runner build --delete-conflicting-outputs
@echo "$(GREEN)✅ Flutter code generation complete$(NC)"
# Clean everything # Clean everything
clean: clean:
@echo "$(YELLOW)WARNING: This will remove all containers and volumes!$(NC)" @echo "$(YELLOW)WARNING: This will remove all containers and volumes!$(NC)"
@@ -218,8 +196,6 @@ services-up:
dev-billing-documents \ dev-billing-documents \
dev-ledger \ dev-ledger \
dev-payments-orchestrator \ dev-payments-orchestrator \
dev-payments-quotation \
dev-payments-methods \
dev-chain-gateway \ dev-chain-gateway \
dev-tron-gateway \ dev-tron-gateway \
dev-mntx-gateway \ dev-mntx-gateway \
@@ -247,8 +223,6 @@ list-services:
@echo " - dev-billing-documents :50061, :9409 (Billing Documents)" @echo " - dev-billing-documents :50061, :9409 (Billing Documents)"
@echo " - dev-ledger :50052, :9401 (Double-Entry Ledger)" @echo " - dev-ledger :50052, :9401 (Double-Entry Ledger)"
@echo " - dev-payments-orchestrator :50062, :9403 (Payment Orchestration)" @echo " - dev-payments-orchestrator :50062, :9403 (Payment Orchestration)"
@echo " - dev-payments-quotation :50064, :9414 (Payment Quotation)"
@echo " - dev-payments-methods :50066, :9416 (Payment Methods)"
@echo " - dev-chain-gateway :50070, :9404 (EVM Blockchain Gateway)" @echo " - dev-chain-gateway :50070, :9404 (EVM Blockchain Gateway)"
@echo " - dev-tron-gateway :50071, :9408 (TRON Blockchain Gateway)" @echo " - dev-tron-gateway :50071, :9408 (TRON Blockchain Gateway)"
@echo " - dev-mntx-gateway :50075, :9405, :8084 (Card Payouts)" @echo " - dev-mntx-gateway :50075, :9405, :8084 (Card Payouts)"
@@ -277,7 +251,7 @@ build-fx:
build-payments: build-payments:
@echo "$(GREEN)Building payment services...$(NC)" @echo "$(GREEN)Building payment services...$(NC)"
@$(COMPOSE) build dev-payments-orchestrator dev-payments-quotation dev-payments-methods @$(COMPOSE) build dev-payments-orchestrator
build-gateways: build-gateways:
@echo "$(GREEN)Building gateway services...$(NC)" @echo "$(GREEN)Building gateway services...$(NC)"
@@ -290,46 +264,3 @@ build-api:
build-frontend: build-frontend:
@echo "$(GREEN)Building frontend...$(NC)" @echo "$(GREEN)Building frontend...$(NC)"
@$(COMPOSE) build dev-frontend @$(COMPOSE) build dev-frontend
# Update all dependencies
update: update-api update-frontend
# Update Go API dependencies
update-api:
@echo "$(GREEN)Updating Go dependencies...$(NC)"
@for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Updating $$dir..."; \
(cd "$$dir" && go get -u ./... && go mod tidy); \
done
@echo "$(GREEN)✅ Go dependencies updated$(NC)"
# Update Flutter dependencies
update-frontend:
@echo "$(GREEN)Updating Flutter dependencies...$(NC)"
@cd frontend/pshared && flutter pub upgrade --major-versions
@cd frontend/pweb && flutter pub upgrade --major-versions
@echo "$(GREEN)✅ Flutter dependencies updated$(NC)"
# Run all tests
test: test-api test-frontend
# Run Go API tests
test-api:
@echo "$(GREEN)Running API tests...$(NC)"
@failed=""; \
for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Testing $$dir..."; \
(cd "$$dir" && go test ./...) || failed="$$failed $$dir"; \
done; \
if [ -n "$$failed" ]; then \
echo "$(YELLOW)Failed:$$failed$(NC)"; \
exit 1; \
fi
@echo "$(GREEN)✅ All API tests passed$(NC)"
# Run Flutter tests
test-frontend:
@echo "$(GREEN)Running frontend tests...$(NC)"
@cd frontend/pshared && flutter test
@cd frontend/pweb && flutter test
@echo "$(GREEN)✅ All frontend tests passed$(NC)"

100
README.md
View File

@@ -1,100 +0,0 @@
# Sendico [![Build Status](https://ci.sendico.io/api/badges/1/status.svg?branch=main)](https://ci.sendico.io/repos/1)
Financial services platform providing payment orchestration, ledger accounting, FX conversion, and multi-rail payment processing.
## Architecture
- **Backend**: Go microservices with gRPC inter-service communication
- **Frontend**: Flutter/Dart web application
- **Infrastructure**: Woodpecker CI/CD, Docker, MongoDB, NATS, Vault
## Services
| Service | Path | Description |
|---------|------|-------------|
| Discovery | `api/discovery/` | Service registry |
| Ledger | `api/ledger/` | Double-entry accounting |
| Orchestrator | `api/payments/orchestrator/` | Payment orchestration |
| Quotation | `api/payments/quotation/` | Payment quotation |
| Payment Methods | `api/payments/methods/` | Payment methods |
| Billing Fees | `api/billing/fees/` | Fee calculation |
| Billing Documents | `api/billing/documents/` | Billing documents |
| FX Oracle | `api/fx/oracle/` | FX quote provider |
| FX Ingestor | `api/fx/ingestor/` | FX rate ingestion |
| Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway |
| Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway |
| Gateway MNTX | `api/gateway/mntx/` | Card payouts |
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
| Notification | `api/notification/` | Notifications |
| BFF | `api/server/` | Backend for frontend |
| Frontend | `frontend/pweb/` | Flutter web UI |
## Development
Development uses Docker Compose via the Makefile. Run `make help` for all available commands.
### Quick Start
```bash
make init # First-time setup (generates keys, .env.dev, builds images)
make up # Start all services
make vault-init # Initialize Vault (if needed)
```
### Common Commands
```bash
make build # Build all service images
make up # Start all services
make down # Stop all services
make restart # Restart all services
make status # Show service status
make logs # View all logs
make logs SERVICE=dev-ledger # View logs for a specific service
make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service
make clean # Remove all containers and volumes
```
### Selective Start
```bash
make infra-up # Start infrastructure only (MongoDB, NATS, Vault)
make services-up # Start application services only (assumes infra is running)
```
### Build Groups
```bash
make build-core # discovery, ledger, fees, documents
make build-fx # oracle, ingestor
make build-payments # orchestrator
make build-gateways # chain, tron, mntx, tgsettle
make build-api # notification, bff
make build-frontend # Flutter web UI
```
### Code Generation
```bash
make generate # Generate all code (protobuf + Flutter)
make generate-api # Generate protobuf code only
make generate-frontend # Generate Flutter code only (build_runner)
make proto # Alias for generate-api
```
### Testing
```bash
make test # Run all tests (API + frontend)
make test-api # Run Go API tests only
make test-frontend # Run Flutter tests only
```
### Update Dependencies
```bash
make update # Update all Go and Flutter dependencies
make update-api # Update Go dependencies only
make update-frontend # Update Flutter dependencies only
```

View File

@@ -1,196 +0,0 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- wsl
- wrapcheck
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- funlen
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

View File

@@ -1,70 +1,70 @@
module github.com/tech/sendico/billing/documents module github.com/tech/sendico/billing/documents
go 1.25.7 go 1.25.6
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
require ( require (
github.com/aws/aws-sdk-go-v2 v1.41.2 github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/config v1.32.7
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 github.com/aws/aws-sdk-go-v2/credentials v1.19.7
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
github.com/jung-kurt/gofpdf v1.16.2 github.com/jung-kurt/gofpdf v1.16.2
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.78.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.1 // indirect github.com/aws/smithy-go v1.24.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-chi/chi/v5 v5.2.4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -4,44 +4,44 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@@ -78,8 +78,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -100,8 +100,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -134,8 +134,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -158,8 +158,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
@@ -201,16 +201,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/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 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -221,16 +221,16 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -241,16 +241,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -258,10 +258,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -24,6 +24,5 @@ func Create() version.Printer {
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&info) return vf.Create(&info)
} }

View File

@@ -21,13 +21,11 @@ func NewLocalStore(logger mlogger.Logger, cfg LocalConfig) (*LocalStore, error)
if root == "" { if root == "" {
return nil, merrors.InvalidArgument("docstore: local root_path is empty") return nil, merrors.InvalidArgument("docstore: local root_path is empty")
} }
store := &LocalStore{ store := &LocalStore{
logger: logger.Named("docstore").Named("local"), logger: logger.Named("docstore").Named("local"),
rootPath: root, rootPath: root,
} }
store.logger.Info("Document storage initialised", zap.String("root_path", root)) store.logger.Info("Document storage initialised", zap.String("root_path", root))
return store, nil return store, nil
} }
@@ -35,19 +33,15 @@ func (s *LocalStore) Save(ctx context.Context, key string, data []byte) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
path := filepath.Join(s.rootPath, filepath.Clean(key)) path := filepath.Join(s.rootPath, filepath.Clean(key))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path)) s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path))
return err return err
} }
if err := os.WriteFile(path, data, 0o600); err != nil { if err := os.WriteFile(path, data, 0o600); err != nil {
s.logger.Warn("Failed to write document file", zap.Error(err), zap.String("path", path)) s.logger.Warn("Failed to write document file", zap.Error(err), zap.String("path", path))
return err return err
} }
return nil return nil
} }
@@ -55,16 +49,12 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
path := filepath.Join(s.rootPath, filepath.Clean(key)) path := filepath.Join(s.rootPath, filepath.Clean(key))
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path)) s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path))
return nil, err return nil, err
} }
return data, nil return data, nil
} }

View File

@@ -32,7 +32,6 @@ func NewS3Store(logger mlogger.Logger, cfg S3Config) (*S3Store, error) {
if accessKey == "" && cfg.AccessKeyEnv != "" { if accessKey == "" && cfg.AccessKeyEnv != "" {
accessKey = strings.TrimSpace(os.Getenv(cfg.AccessKeyEnv)) accessKey = strings.TrimSpace(os.Getenv(cfg.AccessKeyEnv))
} }
secretKey := strings.TrimSpace(cfg.SecretAccessKey) secretKey := strings.TrimSpace(cfg.SecretAccessKey)
if secretKey == "" && cfg.SecretKeyEnv != "" { if secretKey == "" && cfg.SecretKeyEnv != "" {
secretKey = strings.TrimSpace(os.Getenv(cfg.SecretKeyEnv)) secretKey = strings.TrimSpace(os.Getenv(cfg.SecretKeyEnv))
@@ -63,21 +62,23 @@ func NewS3Store(logger mlogger.Logger, cfg S3Config) (*S3Store, error) {
endpoint = "http://" + endpoint endpoint = "http://" + endpoint
} }
} }
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
if service == s3.ServiceID {
return aws.Endpoint{URL: endpoint, SigningRegion: region, HostnameImmutable: true}, nil
}
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
})
loadOpts = append(loadOpts, config.WithEndpointResolverWithOptions(resolver))
} }
awsCfg, err := config.LoadDefaultConfig(context.Background(), loadOpts...) awsCfg, err := config.LoadDefaultConfig(context.Background(), loadOpts...)
if err != nil { if err != nil {
logger.Warn("Failed to create AWS config", zap.Error(err), zap.String("bucket", bucket)) logger.Warn("Failed to create AWS config", zap.Error(err), zap.String("bucket", bucket))
return nil, err return nil, err
} }
client := s3.NewFromConfig(awsCfg, func(opts *s3.Options) { client := s3.NewFromConfig(awsCfg, func(opts *s3.Options) {
opts.UsePathStyle = cfg.ForcePathStyle opts.UsePathStyle = cfg.ForcePathStyle
if endpoint != "" {
opts.BaseEndpoint = aws.String(endpoint)
}
}) })
store := &S3Store{ store := &S3Store{
@@ -86,7 +87,6 @@ func NewS3Store(logger mlogger.Logger, cfg S3Config) (*S3Store, error) {
bucket: bucket, bucket: bucket,
} }
store.logger.Info("Document storage initialised", zap.String("bucket", bucket), zap.String("endpoint", endpoint)) store.logger.Info("Document storage initialised", zap.String("bucket", bucket), zap.String("endpoint", endpoint))
return store, nil return store, nil
} }
@@ -94,7 +94,6 @@ func (s *S3Store) Save(ctx context.Context, key string, data []byte) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{ _, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket), Bucket: aws.String(s.bucket),
Key: aws.String(key), Key: aws.String(key),
@@ -102,10 +101,8 @@ func (s *S3Store) Save(ctx context.Context, key string, data []byte) error {
}) })
if err != nil { if err != nil {
s.logger.Warn("Failed to upload document", zap.Error(err), zap.String("key", key)) s.logger.Warn("Failed to upload document", zap.Error(err), zap.String("key", key))
return err return err
} }
return nil return nil
} }
@@ -113,19 +110,15 @@ func (s *S3Store) Load(ctx context.Context, key string) ([]byte, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
obj, err := s.client.GetObject(ctx, &s3.GetObjectInput{ obj, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket), Bucket: aws.String(s.bucket),
Key: aws.String(key), Key: aws.String(key),
}) })
if err != nil { if err != nil {
s.logger.Warn("Failed to fetch document", zap.Error(err), zap.String("key", key)) s.logger.Warn("Failed to fetch document", zap.Error(err), zap.String("key", key))
return nil, err return nil, err
} }
defer obj.Body.Close() defer obj.Body.Close()
return io.ReadAll(obj.Body) return io.ReadAll(obj.Body)
} }

View File

@@ -36,7 +36,7 @@ type S3Config struct {
Bucket string `yaml:"bucket"` Bucket string `yaml:"bucket"`
AccessKeyEnv string `yaml:"access_key_env"` AccessKeyEnv string `yaml:"access_key_env"`
SecretKeyEnv string `yaml:"secret_access_key_env"` SecretKeyEnv string `yaml:"secret_access_key_env"`
AccessKey string `yaml:"access_key"` //nolint:gosec // config field, not a hardcoded secret AccessKey string `yaml:"access_key"`
SecretAccessKey string `yaml:"secret_access_key"` SecretAccessKey string `yaml:"secret_access_key"`
UseSSL bool `yaml:"use_ssl"` UseSSL bool `yaml:"use_ssl"`
ForcePathStyle bool `yaml:"force_path_style"` ForcePathStyle bool `yaml:"force_path_style"`
@@ -55,13 +55,11 @@ func New(logger mlogger.Logger, cfg Config) (Store, error) {
if cfg.Local == nil { if cfg.Local == nil {
return nil, merrors.InvalidArgument("docstore: local config missing") return nil, merrors.InvalidArgument("docstore: local config missing")
} }
return NewLocalStore(logger, *cfg.Local) return NewLocalStore(logger, *cfg.Local)
case string(DriverS3), string(DriverMinio): case string(DriverS3), string(DriverMinio):
if cfg.S3 == nil { if cfg.S3 == nil {
return nil, merrors.InvalidArgument("docstore: s3 config missing") return nil, merrors.InvalidArgument("docstore: s3 config missing")
} }
return NewS3Store(logger, *cfg.S3) return NewS3Store(logger, *cfg.S3)
default: default:
return nil, merrors.InvalidArgument("docstore: unsupported driver") return nil, merrors.InvalidArgument("docstore: unsupported driver")

View File

@@ -29,8 +29,7 @@ type Imp struct {
type config struct { type config struct {
*grpcapp.Config `yaml:",inline"` *grpcapp.Config `yaml:",inline"`
Documents documents.Config `yaml:"documents"`
Documents documents.Config `yaml:"documents"`
} }
// Create initialises the billing documents server implementation. // Create initialises the billing documents server implementation.
@@ -47,7 +46,6 @@ func (i *Imp) Shutdown() {
if i.service != nil { if i.service != nil {
i.service.Shutdown() i.service.Shutdown()
} }
return return
} }
@@ -70,7 +68,6 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.config = cfg i.config = cfg
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
@@ -80,23 +77,20 @@ func (i *Imp) Start() error {
docStore, err := docstore.New(i.logger, cfg.Documents.Storage) docStore, err := docstore.New(i.logger, cfg.Documents.Storage)
if err != nil { if err != nil {
i.logger.Error("Failed to initialise document storage", zap.Error(err)) i.logger.Error("Failed to initialise document storage", zap.Error(err))
return err return err
} }
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { //nolint:lll // factory signature dictated by grpcapp serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
invokeURI := "" invokeURI := ""
if cfg.GRPC != nil { if cfg.GRPC != nil {
invokeURI = cfg.GRPC.DiscoveryInvokeURI() invokeURI = cfg.GRPC.DiscoveryInvokeURI()
} }
svc := documents.NewService(logger, repo, producer, svc := documents.NewService(logger, repo, producer,
documents.WithDiscoveryInvokeURI(invokeURI), documents.WithDiscoveryInvokeURI(invokeURI),
documents.WithConfig(cfg.Documents), documents.WithConfig(cfg.Documents),
documents.WithDocumentStore(docStore), documents.WithDocumentStore(docStore),
) )
i.service = svc i.service = svc
return svc, nil return svc, nil
} }
@@ -104,7 +98,6 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.app = app i.app = app
return i.app.Start() return i.app.Start()
@@ -114,14 +107,12 @@ func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err return nil, err
} }
cfg := &config{Config: &grpcapp.Config{}} cfg := &config{Config: &grpcapp.Config{}}
if err := yaml.Unmarshal(data, cfg); err != nil { if err := yaml.Unmarshal(data, cfg); err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err return nil, err
} }

View File

@@ -29,6 +29,5 @@ func (c Config) AcceptanceTemplatePath() string {
if strings.TrimSpace(c.Templates.AcceptancePath) == "" { if strings.TrimSpace(c.Templates.AcceptancePath) == "" {
return "templates/acceptance.tpl" return "templates/acceptance.tpl"
} }
return c.Templates.AcceptancePath return c.Templates.AcceptancePath
} }

View File

@@ -85,18 +85,14 @@ func statusFromError(err error) string {
if err == nil { if err == nil {
return "success" return "success"
} }
st, ok := status.FromError(err) st, ok := status.FromError(err)
if !ok { if !ok {
return "error" return "error"
} }
code := st.Code() code := st.Code()
if code == codes.OK { if code == codes.OK {
return "success" return "success"
} }
return strings.ToLower(code.String()) return strings.ToLower(code.String())
} }
@@ -105,6 +101,5 @@ func docTypeLabel(docType documentsv1.DocumentType) string {
if label == "" { if label == "" {
return "DOCUMENT_TYPE_UNSPECIFIED" return "DOCUMENT_TYPE_UNSPECIFIED"
} }
return label return label
} }

View File

@@ -41,7 +41,6 @@ func WithDiscoveryInvokeURI(uri string) Option {
if s == nil { if s == nil {
return return
} }
s.invokeURI = strings.TrimSpace(uri) s.invokeURI = strings.TrimSpace(uri)
} }
} }
@@ -52,7 +51,6 @@ func WithProducer(producer msg.Producer) Option {
if s == nil { if s == nil {
return return
} }
s.producer = producer s.producer = producer
} }
} }
@@ -63,7 +61,6 @@ func WithConfig(cfg Config) Option {
if s == nil { if s == nil {
return return
} }
s.config = cfg s.config = cfg
} }
} }
@@ -74,7 +71,6 @@ func WithDocumentStore(store docstore.Store) Option {
if s == nil { if s == nil {
return return
} }
s.docStore = store s.docStore = store
} }
} }
@@ -85,15 +81,12 @@ func WithTemplateRenderer(renderer TemplateRenderer) Option {
if s == nil { if s == nil {
return return
} }
s.template = renderer s.template = renderer
} }
} }
// Service provides billing document metadata and retrieval endpoints. // Service provides billing document metadata and retrieval endpoints.
type Service struct { type Service struct {
documentsv1.UnimplementedDocumentServiceServer
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
docStore docstore.Store docStore docstore.Store
@@ -102,12 +95,12 @@ type Service struct {
invokeURI string invokeURI string
config Config config Config
template TemplateRenderer template TemplateRenderer
documentsv1.UnimplementedDocumentServiceServer
} }
// NewService constructs a documents service with optional configuration. // NewService constructs a documents service with optional configuration.
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service { func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
initMetrics() initMetrics()
svc := &Service{ svc := &Service{
logger: logger.Named("documents"), logger: logger.Named("documents"),
storage: repo, storage: repo,
@@ -116,17 +109,14 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
for _, opt := range opts { for _, opt := range opts {
opt(svc) opt(svc)
} }
if svc.template == nil { if svc.template == nil {
if tmpl, err := newTemplateRenderer(svc.config.AcceptanceTemplatePath()); err != nil { if tmpl, err := newTemplateRenderer(svc.config.AcceptanceTemplatePath()); err != nil {
svc.logger.Warn("Failed to load acceptance template", zap.Error(err)) svc.logger.Warn("failed to load acceptance template", zap.Error(err))
} else { } else {
svc.template = tmpl svc.template = tmpl
} }
} }
svc.startDiscoveryAnnouncer() svc.startDiscoveryAnnouncer()
return svc return svc
} }
@@ -140,22 +130,32 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
if s.announcer != nil { if s.announcer != nil {
s.announcer.Stop() s.announcer.Stop()
} }
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: "BILLING_DOCUMENTS",
Operations: []string{"documents.batch_resolve", "documents.get"},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.BillingDocuments), announce)
s.announcer.Start()
}
func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) { func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) {
start := time.Now() start := time.Now()
var paymentRefs []string var paymentRefs []string
if req != nil { if req != nil {
paymentRefs = req.GetPaymentRefs() paymentRefs = req.GetPaymentRefs()
} }
logger := s.logger.With(zap.Int("payment_refs", len(paymentRefs))) logger := s.logger.With(zap.Int("payment_refs", len(paymentRefs)))
defer func() { defer func() {
statusLabel := statusFromError(err) statusLabel := statusFromError(err)
observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start)) observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start))
@@ -165,48 +165,38 @@ func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.Ba
if resp != nil { if resp != nil {
itemsCount = len(resp.GetItems()) itemsCount = len(resp.GetItems())
} }
fields := []zap.Field{ fields := []zap.Field{
zap.String("status", statusLabel), zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)), zap.Duration("duration", time.Since(start)),
zap.Int("items", itemsCount), zap.Int("items", itemsCount),
} }
if err != nil { if err != nil {
logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...) logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...)
return return
} }
logger.Info("BatchResolveDocuments finished", fields...) logger.Info("BatchResolveDocuments finished", fields...)
}() }()
if len(paymentRefs) == 0 { if len(paymentRefs) == 0 {
resp = &documentsv1.BatchResolveDocumentsResponse{} resp = &documentsv1.BatchResolveDocumentsResponse{}
return resp, nil return resp, nil
} }
if s.storage == nil { if s.storage == nil {
err = status.Error(codes.Unavailable, errStorageUnavailable.Error()) err = status.Error(codes.Unavailable, errStorageUnavailable.Error())
return nil, err return nil, err
} }
refs := make([]string, 0, len(paymentRefs)) refs := make([]string, 0, len(paymentRefs))
for _, ref := range paymentRefs { for _, ref := range paymentRefs {
clean := strings.TrimSpace(ref) clean := strings.TrimSpace(ref)
if clean == "" { if clean == "" {
continue continue
} }
refs = append(refs, clean) refs = append(refs, clean)
} }
if len(refs) == 0 { if len(refs) == 0 {
resp = &documentsv1.BatchResolveDocumentsResponse{} resp = &documentsv1.BatchResolveDocumentsResponse{}
return resp, nil return resp, nil
} }
@@ -216,12 +206,10 @@ func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.Ba
} }
recordByRef := map[string]*model.DocumentRecord{} recordByRef := map[string]*model.DocumentRecord{}
for _, record := range records { for _, record := range records {
if record == nil { if record == nil {
continue continue
} }
recordByRef[record.PaymentRef] = record recordByRef[record.PaymentRef] = record
} }
@@ -230,23 +218,18 @@ func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.Ba
meta := &documentsv1.DocumentMeta{PaymentRef: ref} meta := &documentsv1.DocumentMeta{PaymentRef: ref}
if record := recordByRef[ref]; record != nil { if record := recordByRef[ref]; record != nil {
record.Normalize() record.Normalize()
available := []model.DocumentType{model.DocumentTypeAct} available := []model.DocumentType{model.DocumentTypeAct}
ready := make([]model.DocumentType, 0, 1) ready := make([]model.DocumentType, 0, 1)
if path, ok := record.StoragePaths[model.DocumentTypeAct]; ok && path != "" { if path, ok := record.StoragePaths[model.DocumentTypeAct]; ok && path != "" {
ready = append(ready, model.DocumentTypeAct) ready = append(ready, model.DocumentTypeAct)
} }
meta.AvailableTypes = toProtoTypes(available) meta.AvailableTypes = toProtoTypes(available)
meta.ReadyTypes = toProtoTypes(ready) meta.ReadyTypes = toProtoTypes(ready)
} }
items = append(items, meta) items = append(items, meta)
} }
resp = &documentsv1.BatchResolveDocumentsResponse{Items: items} resp = &documentsv1.BatchResolveDocumentsResponse{Items: items}
return resp, nil return resp, nil
} }
@@ -254,12 +237,10 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
start := time.Now() start := time.Now()
docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
paymentRef := "" paymentRef := ""
if req != nil { if req != nil {
docType = req.GetType() docType = req.GetType()
paymentRef = strings.TrimSpace(req.GetPaymentRef()) paymentRef = strings.TrimSpace(req.GetPaymentRef())
} }
logger := s.logger.With( logger := s.logger.With(
zap.String("payment_ref", paymentRef), zap.String("payment_ref", paymentRef),
zap.String("document_type", docTypeLabel(docType)), zap.String("document_type", docTypeLabel(docType)),
@@ -268,7 +249,6 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
defer func() { defer func() {
statusLabel := statusFromError(err) statusLabel := statusFromError(err)
observeRequest("get_document", docType, statusLabel, time.Since(start)) observeRequest("get_document", docType, statusLabel, time.Since(start))
if resp != nil { if resp != nil {
observeDocumentBytes(docType, len(resp.GetContent())) observeDocumentBytes(docType, len(resp.GetContent()))
} }
@@ -277,49 +257,36 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
if resp != nil { if resp != nil {
contentBytes = len(resp.GetContent()) contentBytes = len(resp.GetContent())
} }
fields := []zap.Field{ fields := []zap.Field{
zap.String("status", statusLabel), zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)), zap.Duration("duration", time.Since(start)),
zap.Int("content_bytes", contentBytes), zap.Int("content_bytes", contentBytes),
} }
if err != nil { if err != nil {
logger.Warn("GetDocument failed", append(fields, zap.Error(err))...) logger.Warn("GetDocument failed", append(fields, zap.Error(err))...)
return return
} }
logger.Info("GetDocument finished", fields...) logger.Info("GetDocument finished", fields...)
}() }()
if paymentRef == "" { if paymentRef == "" {
err = status.Error(codes.InvalidArgument, "payment_ref is required") err = status.Error(codes.InvalidArgument, "payment_ref is required")
return nil, err return nil, err
} }
if docType == documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED { if docType == documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED {
err = status.Error(codes.InvalidArgument, "document type is required") err = status.Error(codes.InvalidArgument, "document type is required")
return nil, err return nil, err
} }
if s.storage == nil { if s.storage == nil {
err = status.Error(codes.Unavailable, errStorageUnavailable.Error()) err = status.Error(codes.Unavailable, errStorageUnavailable.Error())
return nil, err return nil, err
} }
if s.docStore == nil { if s.docStore == nil {
err = status.Error(codes.Unavailable, errDocStoreUnavailable.Error()) err = status.Error(codes.Unavailable, errDocStoreUnavailable.Error())
return nil, err return nil, err
} }
if s.template == nil { if s.template == nil {
err = status.Error(codes.FailedPrecondition, errTemplateUnavailable.Error()) err = status.Error(codes.FailedPrecondition, errTemplateUnavailable.Error())
return nil, err return nil, err
} }
@@ -328,10 +295,8 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
if errors.Is(err, storage.ErrDocumentNotFound) { if errors.Is(err, storage.ErrDocumentNotFound) {
return nil, status.Error(codes.NotFound, "document record not found") return nil, status.Error(codes.NotFound, "document record not found")
} }
return nil, status.Error(codes.Internal, err.Error()) return nil, status.Error(codes.Internal, err.Error())
} }
record.Normalize() record.Normalize()
targetType := model.DocumentTypeFromProto(docType) targetType := model.DocumentTypeFromProto(docType)
@@ -345,7 +310,6 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
if loadErr != nil { if loadErr != nil {
return nil, status.Error(codes.Internal, loadErr.Error()) return nil, status.Error(codes.Internal, loadErr.Error())
} }
return &documentsv1.GetDocumentResponse{ return &documentsv1.GetDocumentResponse{
Content: content, Content: content,
Filename: documentFilename(docType, paymentRef), Filename: documentFilename(docType, paymentRef),
@@ -356,23 +320,19 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
content, hash, genErr := s.generateActPDF(record.Snapshot) content, hash, genErr := s.generateActPDF(record.Snapshot)
if genErr != nil { if genErr != nil {
logger.Warn("Failed to generate document", zap.Error(genErr)) logger.Warn("Failed to generate document", zap.Error(genErr))
return nil, status.Error(codes.Internal, genErr.Error()) return nil, status.Error(codes.Internal, genErr.Error())
} }
path := documentStoragePath(paymentRef, docType) path := documentStoragePath(paymentRef, docType)
if saveErr := s.docStore.Save(ctx, path, content); saveErr != nil { if saveErr := s.docStore.Save(ctx, path, content); saveErr != nil {
logger.Warn("Failed to store document", zap.Error(saveErr)) logger.Warn("Failed to store document", zap.Error(saveErr))
return nil, status.Error(codes.Internal, saveErr.Error()) return nil, status.Error(codes.Internal, saveErr.Error())
} }
record.StoragePaths[targetType] = path record.StoragePaths[targetType] = path
record.Hashes[targetType] = hash record.Hashes[targetType] = hash
if updateErr := s.storage.Documents().Update(ctx, record); updateErr != nil { if updateErr := s.storage.Documents().Update(ctx, record); updateErr != nil {
logger.Warn("Failed to update document record", zap.Error(updateErr)) logger.Warn("Failed to update document record", zap.Error(updateErr))
return nil, status.Error(codes.Internal, updateErr.Error()) return nil, status.Error(codes.Internal, updateErr.Error())
} }
@@ -381,25 +341,9 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
Filename: documentFilename(docType, paymentRef), Filename: documentFilename(docType, paymentRef),
MimeType: "application/pdf", MimeType: "application/pdf",
} }
return resp, nil return resp, nil
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: "BILLING_DOCUMENTS",
Operations: []string{discovery.OperationDocumentsBatchResolve, discovery.OperationDocumentsGet},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.BillingDocuments, announce)
s.announcer.Start()
}
type serviceError string type serviceError string
func (e serviceError) Error() string { func (e serviceError) Error() string {
@@ -417,18 +361,15 @@ func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, er
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
generated := renderer.Renderer{ generated := renderer.Renderer{
Issuer: s.config.Issuer, Issuer: s.config.Issuer,
OwnerPassword: s.config.Protection.OwnerPassword, OwnerPassword: s.config.Protection.OwnerPassword,
} }
placeholder := strings.Repeat("0", 64) placeholder := strings.Repeat("0", 64)
firstPass, err := generated.Render(blocks, placeholder) firstPass, err := generated.Render(blocks, placeholder)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
footerHash := sha256.Sum256(firstPass) footerHash := sha256.Sum256(firstPass)
footerHex := hex.EncodeToString(footerHash[:]) footerHex := hex.EncodeToString(footerHash[:])
@@ -436,7 +377,6 @@ func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, er
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
return finalBytes, footerHex, nil return finalBytes, footerHex, nil
} }
@@ -444,18 +384,15 @@ func toProtoTypes(types []model.DocumentType) []documentsv1.DocumentType {
if len(types) == 0 { if len(types) == 0 {
return nil return nil
} }
result := make([]documentsv1.DocumentType, 0, len(types)) result := make([]documentsv1.DocumentType, 0, len(types))
for _, t := range types { for _, t := range types {
result = append(result, t.Proto()) result = append(result, t.Proto())
} }
return result return result
} }
func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string { func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string {
suffix := "document.pdf" suffix := "document.pdf"
switch docType { switch docType {
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT: case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
suffix = "act.pdf" suffix = "act.pdf"
@@ -463,16 +400,12 @@ func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) st
suffix = "invoice.pdf" suffix = "invoice.pdf"
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT: case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
suffix = "receipt.pdf" suffix = "receipt.pdf"
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
// default suffix used
} }
return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix)) return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix))
} }
func documentFilename(docType documentsv1.DocumentType, paymentRef string) string { func documentFilename(docType documentsv1.DocumentType, paymentRef string) string {
name := "document" name := "document"
switch docType { switch docType {
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT: case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
name = "act" name = "act"
@@ -480,9 +413,6 @@ func documentFilename(docType documentsv1.DocumentType, paymentRef string) strin
name = "invoice" name = "invoice"
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT: case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
name = "receipt" name = "receipt"
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
// default name used
} }
return fmt.Sprintf("%s_%s.pdf", name, paymentRef) return fmt.Sprintf("%s_%s.pdf", name, paymentRef)
} }

View File

@@ -18,7 +18,7 @@ type stubRepo struct {
store storage.DocumentsStore store storage.DocumentsStore
} }
func (s *stubRepo) Ping(_ context.Context) error { return nil } func (s *stubRepo) Ping(ctx context.Context) error { return nil }
func (s *stubRepo) Documents() storage.DocumentsStore { return s.store } func (s *stubRepo) Documents() storage.DocumentsStore { return s.store }
var _ storage.Repository = (*stubRepo)(nil) var _ storage.Repository = (*stubRepo)(nil)
@@ -28,24 +28,22 @@ type stubDocumentsStore struct {
updateCalls int updateCalls int
} }
func (s *stubDocumentsStore) Create(_ context.Context, record *model.DocumentRecord) error { func (s *stubDocumentsStore) Create(ctx context.Context, record *model.DocumentRecord) error {
s.record = record s.record = record
return nil return nil
} }
func (s *stubDocumentsStore) Update(_ context.Context, record *model.DocumentRecord) error { func (s *stubDocumentsStore) Update(ctx context.Context, record *model.DocumentRecord) error {
s.record = record s.record = record
s.updateCalls++ s.updateCalls++
return nil return nil
} }
func (s *stubDocumentsStore) GetByPaymentRef(_ context.Context, _ string) (*model.DocumentRecord, error) { func (s *stubDocumentsStore) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.DocumentRecord, error) {
return s.record, nil return s.record, nil
} }
func (s *stubDocumentsStore) ListByPaymentRefs(_ context.Context, _ []string) ([]*model.DocumentRecord, error) { func (s *stubDocumentsStore) ListByPaymentRefs(ctx context.Context, paymentRefs []string) ([]*model.DocumentRecord, error) {
return []*model.DocumentRecord{s.record}, nil return []*model.DocumentRecord{s.record}, nil
} }
@@ -61,21 +59,19 @@ func newMemDocStore() *memDocStore {
return &memDocStore{data: map[string][]byte{}} return &memDocStore{data: map[string][]byte{}}
} }
func (m *memDocStore) Save(_ context.Context, key string, data []byte) error { func (m *memDocStore) Save(ctx context.Context, key string, data []byte) error {
m.saveCount++ m.saveCount++
copyData := make([]byte, len(data)) copyData := make([]byte, len(data))
copy(copyData, data) copy(copyData, data)
m.data[key] = copyData m.data[key] = copyData
return nil return nil
} }
func (m *memDocStore) Load(_ context.Context, key string) ([]byte, error) { func (m *memDocStore) Load(ctx context.Context, key string) ([]byte, error) {
m.loadCount++ m.loadCount++
data := m.data[key] data := m.data[key]
copyData := make([]byte, len(data)) copyData := make([]byte, len(data))
copy(copyData, data) copy(copyData, data)
return copyData, nil return copyData, nil
} }
@@ -88,9 +84,8 @@ type stubTemplate struct {
calls int calls int
} }
func (s *stubTemplate) Render(_ model.ActSnapshot) ([]renderer.Block, error) { func (s *stubTemplate) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) {
s.calls++ s.calls++
return s.blocks, nil return s.blocks, nil
} }
@@ -140,23 +135,18 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("GetDocument first call: %v", err) t.Fatalf("GetDocument first call: %v", err)
} }
if len(resp1.Content) == 0 {
if len(resp1.GetContent()) == 0 {
t.Fatalf("expected content on first call") t.Fatalf("expected content on first call")
} }
stored := record.Hashes[model.DocumentTypeAct] stored := record.Hashes[model.DocumentTypeAct]
if stored == "" { if stored == "" {
t.Fatalf("expected stored hash") t.Fatalf("expected stored hash")
} }
footerHash := extractFooterHash(resp1.Content)
footerHash := extractFooterHash(resp1.GetContent())
if footerHash == "" { if footerHash == "" {
t.Fatalf("expected footer hash in PDF") t.Fatalf("expected footer hash in PDF")
} }
if stored != footerHash { if stored != footerHash {
t.Fatalf("stored hash mismatch: got %s", stored) t.Fatalf("stored hash mismatch: got %s", stored)
} }
@@ -168,19 +158,16 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("GetDocument second call: %v", err) t.Fatalf("GetDocument second call: %v", err)
} }
if !bytes.Equal(resp1.Content, resp2.Content) {
if !bytes.Equal(resp1.GetContent(), resp2.GetContent()) {
t.Fatalf("expected identical PDF bytes on second call") t.Fatalf("expected identical PDF bytes on second call")
} }
if tmpl.calls != 1 { if tmpl.calls != 1 {
t.Fatalf("expected template to be rendered once, got %d", tmpl.calls) t.Fatalf("expected template to be rendered once, got %d", tmpl.calls)
} }
if store.saveCount != 1 { if store.saveCount != 1 {
t.Fatalf("expected document save once, got %d", store.saveCount) t.Fatalf("expected document save once, got %d", store.saveCount)
} }
if store.loadCount == 0 { if store.loadCount == 0 {
t.Fatalf("expected document load on second call") t.Fatalf("expected document load on second call")
} }
@@ -189,23 +176,17 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
func extractFooterHash(pdf []byte) string { func extractFooterHash(pdf []byte) string {
prefix := []byte("Document integrity hash: ") prefix := []byte("Document integrity hash: ")
idx := bytes.Index(pdf, prefix) idx := bytes.Index(pdf, prefix)
if idx == -1 { if idx == -1 {
return "" return ""
} }
start := idx + len(prefix) start := idx + len(prefix)
end := start end := start
for end < len(pdf) && isHexDigit(pdf[end]) { for end < len(pdf) && isHexDigit(pdf[end]) {
end++ end++
} }
if end-start != 64 { if end-start != 64 {
return "" return ""
} }
return string(pdf[start:end]) return string(pdf[start:end])
} }

View File

@@ -41,7 +41,6 @@ func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block,
if err := r.tpl.Execute(&buf, snapshot); err != nil { if err := r.tpl.Execute(&buf, snapshot); err != nil {
return nil, fmt.Errorf("execute template: %w", err) return nil, fmt.Errorf("execute template: %w", err)
} }
return renderer.ParseBlocks(buf.String()) return renderer.ParseBlocks(buf.String())
} }
@@ -50,7 +49,6 @@ func formatMoney(amount decimal.Decimal, currency string) string {
if currency == "" { if currency == "" {
return amount.String() return amount.String()
} }
return fmt.Sprintf("%s %s", amount.String(), currency) return fmt.Sprintf("%s %s", amount.String(), currency)
} }
@@ -58,6 +56,5 @@ func formatDate(t time.Time) string {
if t.IsZero() { if t.IsZero() {
return "" return ""
} }
return t.Format("2006-01-02") return t.Format("2006-01-02")
} }

View File

@@ -2,7 +2,6 @@ package documents
import ( import (
"path/filepath" "path/filepath"
"slices"
"testing" "testing"
"time" "time"
@@ -13,7 +12,6 @@ import (
func TestTemplateRenderer_Render(t *testing.T) { func TestTemplateRenderer_Render(t *testing.T) {
path := filepath.Join("..", "..", "..", "templates", "acceptance.tpl") path := filepath.Join("..", "..", "..", "templates", "acceptance.tpl")
tmpl, err := newTemplateRenderer(path) tmpl, err := newTemplateRenderer(path)
if err != nil { if err != nil {
t.Fatalf("newTemplateRenderer: %v", err) t.Fatalf("newTemplateRenderer: %v", err)
@@ -31,18 +29,22 @@ func TestTemplateRenderer_Render(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Render: %v", err) t.Fatalf("Render: %v", err)
} }
if len(blocks) == 0 { if len(blocks) == 0 {
t.Fatalf("expected blocks, got none") t.Fatalf("expected blocks, got none")
} }
title := findBlock(blocks, renderer.TagTitle) title := findBlock(blocks, renderer.TagTitle)
if title == nil { if title == nil {
t.Fatalf("expected title block") t.Fatalf("expected title block")
} }
foundTitle := false
if !slices.Contains(title.Lines, "ACT OF ACCEPTANCE OF SERVICES") { for _, line := range title.Lines {
if line == "ACT OF ACCEPTANCE OF SERVICES" {
foundTitle = true
break
}
}
if !foundTitle {
t.Fatalf("expected title content not found") t.Fatalf("expected title content not found")
} }
@@ -50,17 +52,13 @@ func TestTemplateRenderer_Render(t *testing.T) {
if kv == nil { if kv == nil {
t.Fatalf("expected kv block") t.Fatalf("expected kv block")
} }
foundExecutor := false foundExecutor := false
for _, row := range kv.Rows { for _, row := range kv.Rows {
if len(row) >= 2 && row[0] == "Executor" && row[1] == snapshot.ExecutorFullName { if len(row) >= 2 && row[0] == "Executor" && row[1] == snapshot.ExecutorFullName {
foundExecutor = true foundExecutor = true
break break
} }
} }
if !foundExecutor { if !foundExecutor {
t.Fatalf("expected executor name in kv block") t.Fatalf("expected executor name in kv block")
} }
@@ -69,17 +67,13 @@ func TestTemplateRenderer_Render(t *testing.T) {
if table == nil { if table == nil {
t.Fatalf("expected table block") t.Fatalf("expected table block")
} }
foundAmount := false foundAmount := false
for _, row := range table.Rows { for _, row := range table.Rows {
if len(row) >= 2 && row[1] == "123.45 USD" { if len(row) >= 2 && row[1] == "123.45 USD" {
foundAmount = true foundAmount = true
break break
} }
} }
if !foundAmount { if !foundAmount {
t.Fatalf("expected amount in table block") t.Fatalf("expected amount in table block")
} }
@@ -91,6 +85,5 @@ func findBlock(blocks []renderer.Block, tag renderer.Tag) *renderer.Block {
return &blocks[i] return &blocks[i]
} }
} }
return nil return nil
} }

View File

@@ -27,7 +27,6 @@ func drawHeader(pdf *gofpdf.Fpdf, issuer Issuer, marginLeft, marginTop float64)
if logoWidth > 0 { if logoWidth > 0 {
textX = startX + logoWidth + 6 textX = startX + logoWidth + 6
} }
pdf.SetXY(textX, startY) pdf.SetXY(textX, startY)
pdf.SetFont("Helvetica", "B", 12) pdf.SetFont("Helvetica", "B", 12)
pdf.CellFormat(0, 5, issuer.LegalName, "", 1, "L", false, 0, "") pdf.CellFormat(0, 5, issuer.LegalName, "", 1, "L", false, 0, "")
@@ -40,7 +39,6 @@ func drawHeader(pdf *gofpdf.Fpdf, issuer Issuer, marginLeft, marginTop float64)
} }
currentY := pdf.GetY() currentY := pdf.GetY()
if logoWidth > 0 { if logoWidth > 0 {
logoBottom := startY + logoWidth logoBottom := startY + logoWidth
if logoBottom > currentY { if logoBottom > currentY {

View File

@@ -2,6 +2,7 @@ package renderer
import ( import (
"bytes" "bytes"
"fmt"
"math" "math"
"strings" "strings"
@@ -38,22 +39,18 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
pdf.SetFooterFunc(func() { pdf.SetFooterFunc(func() {
pdf.SetY(-15) pdf.SetY(-15)
pdf.SetFont("Helvetica", "", 8) pdf.SetFont("Helvetica", "", 8)
footer := fmt.Sprintf("Document integrity hash: %s", footerHash)
footer := "Document integrity hash: " + footerHash
pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "") pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "")
}) })
pdf.AddPage() pdf.AddPage()
if _, err := drawHeader(pdf, r.Issuer, pageMarginLeft, pageMarginTop); err != nil { if _, err := drawHeader(pdf, r.Issuer, pageMarginLeft, pageMarginTop); err != nil {
return nil, err return nil, err
} }
pdf.Ln(6) pdf.Ln(6)
for _, block := range blocks { for _, block := range blocks {
renderBlock(pdf, block) renderBlock(pdf, block)
if pdf.Error() != nil { if pdf.Error() != nil {
return nil, pdf.Error() return nil, pdf.Error()
} }
@@ -63,7 +60,6 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
if err := pdf.Output(buf); err != nil { if err := pdf.Output(buf); err != nil {
return nil, err return nil, err
} }
return buf.Bytes(), nil return buf.Bytes(), nil
} }
@@ -73,64 +69,47 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
pdf.Ln(6) pdf.Ln(6)
case TagTitle: case TagTitle:
pdf.SetFont("Helvetica", "B", 14) pdf.SetFont("Helvetica", "B", 14)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(4) pdf.Ln(4)
continue continue
} }
pdf.CellFormat(0, 7, line, "", 1, "C", false, 0, "") pdf.CellFormat(0, 7, line, "", 1, "C", false, 0, "")
} }
pdf.Ln(2) pdf.Ln(2)
case TagSubtitle: case TagSubtitle:
pdf.SetFont("Helvetica", "", 11) pdf.SetFont("Helvetica", "", 11)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(3) pdf.Ln(3)
continue continue
} }
pdf.CellFormat(0, 6, line, "", 1, "C", false, 0, "") pdf.CellFormat(0, 6, line, "", 1, "C", false, 0, "")
} }
pdf.Ln(2) pdf.Ln(2)
case TagMeta: case TagMeta:
pdf.SetFont("Helvetica", "", 9) pdf.SetFont("Helvetica", "", 9)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(2) pdf.Ln(2)
continue continue
} }
pdf.CellFormat(0, 4.5, line, "", 1, "R", false, 0, "") pdf.CellFormat(0, 4.5, line, "", 1, "R", false, 0, "")
} }
pdf.Ln(2) pdf.Ln(2)
case TagSection: case TagSection:
pdf.Ln(2) pdf.Ln(2)
pdf.SetFont("Helvetica", "B", 11) pdf.SetFont("Helvetica", "B", 11)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(3) pdf.Ln(3)
continue continue
} }
pdf.CellFormat(0, 6, line, "", 1, "L", false, 0, "") pdf.CellFormat(0, 6, line, "", 1, "L", false, 0, "")
} }
pdf.Ln(1) pdf.Ln(1)
case TagText: case TagText:
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
text := strings.Join(block.Lines, "\n") text := strings.Join(block.Lines, "\n")
pdf.MultiCell(0, 5, text, "", "L", false) pdf.MultiCell(0, 5, text, "", "L", false)
pdf.Ln(1) pdf.Ln(1)
@@ -140,14 +119,12 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
renderTable(pdf, block) renderTable(pdf, block)
case TagSign: case TagSign:
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
text := strings.Join(block.Lines, "\n") text := strings.Join(block.Lines, "\n")
pdf.MultiCell(0, 6, text, "", "L", false) pdf.MultiCell(0, 6, text, "", "L", false)
pdf.Ln(2) pdf.Ln(2)
default: default:
// Unknown tag: treat as plain text for resilience. // Unknown tag: treat as plain text for resilience.
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
text := strings.Join(block.Lines, "\n") text := strings.Join(block.Lines, "\n")
pdf.MultiCell(0, 5, text, "", "L", false) pdf.MultiCell(0, 5, text, "", "L", false)
pdf.Ln(1) pdf.Ln(1)
@@ -156,7 +133,6 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
func renderKeyValue(pdf *gofpdf.Fpdf, block Block) { func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
usable := usableWidth(pdf) usable := usableWidth(pdf)
keyWidth := math.Round(usable * 0.35) keyWidth := math.Round(usable * 0.35)
valueWidth := usable - keyWidth valueWidth := usable - keyWidth
@@ -166,14 +142,11 @@ func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
if len(row) == 0 { if len(row) == 0 {
continue continue
} }
key := row[0] key := row[0]
value := "" value := ""
if len(row) > 1 { if len(row) > 1 {
value = row[1] value = row[1]
} }
x := pdf.GetX() x := pdf.GetX()
y := pdf.GetY() y := pdf.GetY()
@@ -189,7 +162,6 @@ func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
pdf.SetY(maxFloat(leftY, rightY)) pdf.SetY(maxFloat(leftY, rightY))
} }
pdf.Ln(1) pdf.Ln(1)
} }
@@ -197,7 +169,6 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
if len(block.Rows) == 0 { if len(block.Rows) == 0 {
return return
} }
usable := usableWidth(pdf) usable := usableWidth(pdf)
col1 := math.Round(usable * 0.7) col1 := math.Round(usable * 0.7)
col2 := usable - col1 col2 := usable - col1
@@ -205,11 +176,9 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
header := block.Rows[0] header := block.Rows[0]
pdf.SetFont("Helvetica", "B", 10) pdf.SetFont("Helvetica", "B", 10)
if len(header) > 0 { if len(header) > 0 {
pdf.CellFormat(col1, lineHeight, header[0], "1", 0, "L", false, 0, "") pdf.CellFormat(col1, lineHeight, header[0], "1", 0, "L", false, 0, "")
} }
if len(header) > 1 { if len(header) > 1 {
pdf.CellFormat(col2, lineHeight, header[1], "1", 1, "R", false, 0, "") pdf.CellFormat(col2, lineHeight, header[1], "1", 1, "R", false, 0, "")
} else { } else {
@@ -217,19 +186,15 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
} }
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
for _, row := range block.Rows[1:] { for _, row := range block.Rows[1:] {
colA := "" colA := ""
colB := "" colB := ""
if len(row) > 0 { if len(row) > 0 {
colA = row[0] colA = row[0]
} }
if len(row) > 1 { if len(row) > 1 {
colB = row[1] colB = row[1]
} }
x := pdf.GetX() x := pdf.GetX()
y := pdf.GetY() y := pdf.GetY()
pdf.MultiCell(col1, lineHeight, colA, "1", "L", false) pdf.MultiCell(col1, lineHeight, colA, "1", "L", false)
@@ -239,14 +204,12 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
rightY := pdf.GetY() rightY := pdf.GetY()
pdf.SetY(maxFloat(leftY, rightY)) pdf.SetY(maxFloat(leftY, rightY))
} }
pdf.Ln(2) pdf.Ln(2)
} }
func usableWidth(pdf *gofpdf.Fpdf) float64 { func usableWidth(pdf *gofpdf.Fpdf) float64 {
pageW, _ := pdf.GetPageSize() pageW, _ := pdf.GetPageSize()
left, _, right, _ := pdf.GetMargins() left, _, right, _ := pdf.GetMargins()
return pageW - left - right return pageW - left - right
} }
@@ -254,6 +217,5 @@ func maxFloat(a, b float64) float64 {
if a > b { if a > b {
return a return a
} }
return b return b
} }

View File

@@ -26,13 +26,11 @@ func TestRenderer_RenderContainsText(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Render: %v", err) t.Fatalf("Render: %v", err)
} }
if len(pdfBytes) == 0 { if len(pdfBytes) == 0 {
t.Fatalf("expected PDF bytes") t.Fatalf("expected PDF bytes")
} }
checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", "Document integrity hash"} checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", "Document integrity hash"}
for _, token := range checks { for _, token := range checks {
if !containsPDFText(pdfBytes, token) { if !containsPDFText(pdfBytes, token) {
t.Fatalf("expected PDF to contain %q", token) t.Fatalf("expected PDF to contain %q", token)
@@ -44,29 +42,22 @@ func containsPDFText(pdfBytes []byte, text string) bool {
if bytes.Contains(pdfBytes, []byte(text)) { if bytes.Contains(pdfBytes, []byte(text)) {
return true return true
} }
hexText := hex.EncodeToString([]byte(text)) hexText := hex.EncodeToString([]byte(text))
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(hexText))) { if bytes.Contains(pdfBytes, []byte(strings.ToUpper(hexText))) {
return true return true
} }
if bytes.Contains(pdfBytes, []byte(strings.ToLower(hexText))) { if bytes.Contains(pdfBytes, []byte(strings.ToLower(hexText))) {
return true return true
} }
utf16Bytes := encodeUTF16BE(text, false) utf16Bytes := encodeUTF16BE(text, false)
if bytes.Contains(pdfBytes, utf16Bytes) { if bytes.Contains(pdfBytes, utf16Bytes) {
return true return true
} }
utf16Hex := hex.EncodeToString(utf16Bytes) utf16Hex := hex.EncodeToString(utf16Bytes)
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16Hex))) { if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16Hex))) {
return true return true
} }
if bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16Hex))) { if bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16Hex))) {
return true return true
} }
@@ -75,33 +66,25 @@ func containsPDFText(pdfBytes []byte, text string) bool {
if bytes.Contains(pdfBytes, utf16BytesBOM) { if bytes.Contains(pdfBytes, utf16BytesBOM) {
return true return true
} }
utf16HexBOM := hex.EncodeToString(utf16BytesBOM) utf16HexBOM := hex.EncodeToString(utf16BytesBOM)
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16HexBOM))) { if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16HexBOM))) {
return true return true
} }
return bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16HexBOM))) return bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16HexBOM)))
} }
func encodeUTF16BE(text string, withBOM bool) []byte { func encodeUTF16BE(text string, withBOM bool) []byte {
encoded := utf16.Encode([]rune(text)) encoded := utf16.Encode([]rune(text))
length := len(encoded) * 2 length := len(encoded) * 2
if withBOM { if withBOM {
length += 2 length += 2
} }
out := make([]byte, 0, length) out := make([]byte, 0, length)
if withBOM { if withBOM {
out = append(out, 0xFE, 0xFF) out = append(out, 0xFE, 0xFF)
} }
for _, v := range encoded { for _, v := range encoded {
out = append(out, byte(v>>8), byte(v)) out = append(out, byte(v>>8), byte(v))
} }
return out return out
} }

View File

@@ -32,7 +32,6 @@ type Block struct {
func ParseBlocks(input string) ([]Block, error) { func ParseBlocks(input string) ([]Block, error) {
scanner := bufio.NewScanner(strings.NewReader(input)) scanner := bufio.NewScanner(strings.NewReader(input))
blocks := make([]Block, 0) blocks := make([]Block, 0)
var current *Block var current *Block
flush := func() { flush := func() {
@@ -45,24 +44,17 @@ func ParseBlocks(input string) ([]Block, error) {
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimRight(scanner.Text(), "\r") line := strings.TrimRight(scanner.Text(), "\r")
trimmed := strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") { if strings.HasPrefix(trimmed, "#") {
flush() flush()
tag := Tag(strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))) tag := Tag(strings.TrimSpace(strings.TrimPrefix(trimmed, "#")))
if tag == "" { if tag == "" {
continue continue
} }
if tag == TagSpacer { if tag == TagSpacer {
blocks = append(blocks, Block{Tag: TagSpacer}) blocks = append(blocks, Block{Tag: TagSpacer})
continue continue
} }
current = &Block{Tag: tag} current = &Block{Tag: tag}
continue continue
} }
@@ -70,19 +62,16 @@ func ParseBlocks(input string) ([]Block, error) {
continue continue
} }
switch current.Tag { //nolint:exhaustive // only KV and Table need row parsing switch current.Tag {
case TagKV, TagTable: case TagKV, TagTable:
if trimmed == "" { if trimmed == "" {
continue continue
} }
parts := strings.Split(line, "|") parts := strings.Split(line, "|")
row := make([]string, 0, len(parts)) row := make([]string, 0, len(parts))
for _, part := range parts { for _, part := range parts {
row = append(row, strings.TrimSpace(part)) row = append(row, strings.TrimSpace(part))
} }
current.Rows = append(current.Rows, row) current.Rows = append(current.Rows, row)
default: default:
current.Lines = append(current.Lines, line) current.Lines = append(current.Lines, line)
@@ -94,6 +83,5 @@ func ParseBlocks(input string) ([]Block, error) {
} }
flush() flush()
return blocks, nil return blocks, nil
} }

View File

@@ -28,7 +28,6 @@ func DocumentTypeFromProto(t documentsv1.DocumentType) DocumentType {
if name, ok := documentsv1.DocumentType_name[int32(t)]; ok { if name, ok := documentsv1.DocumentType_name[int32(t)]; ok {
return DocumentType(name) return DocumentType(name)
} }
return DocumentTypeUnspecified return DocumentTypeUnspecified
} }
@@ -37,24 +36,22 @@ func (t DocumentType) Proto() documentsv1.DocumentType {
if value, ok := documentsv1.DocumentType_value[string(t)]; ok { if value, ok := documentsv1.DocumentType_value[string(t)]; ok {
return documentsv1.DocumentType(value) return documentsv1.DocumentType(value)
} }
return documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED return documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
} }
// ActSnapshot captures the immutable data needed to generate an acceptance act. // ActSnapshot captures the immutable data needed to generate an acceptance act.
type ActSnapshot struct { type ActSnapshot struct {
PaymentID string `bson:"paymentId" json:"paymentId"` PaymentID string `bson:"paymentId" json:"paymentId"`
Date time.Time `bson:"date" json:"date"` Date time.Time `bson:"date" json:"date"`
ExecutorFullName string `bson:"executorFullName" json:"executorFullName"` ExecutorFullName string `bson:"executorFullName" json:"executorFullName"`
Amount decimal.Decimal `bson:"amount" json:"amount"` Amount decimal.Decimal `bson:"amount" json:"amount"`
Currency string `bson:"currency" json:"currency"` Currency string `bson:"currency" json:"currency"`
} }
func (s *ActSnapshot) Normalize() { func (s *ActSnapshot) Normalize() {
if s == nil { if s == nil {
return return
} }
s.PaymentID = strings.TrimSpace(s.PaymentID) s.PaymentID = strings.TrimSpace(s.PaymentID)
s.ExecutorFullName = strings.TrimSpace(s.ExecutorFullName) s.ExecutorFullName = strings.TrimSpace(s.ExecutorFullName)
s.Currency = strings.TrimSpace(s.Currency) s.Currency = strings.TrimSpace(s.Currency)
@@ -63,25 +60,21 @@ func (s *ActSnapshot) Normalize() {
// DocumentRecord stores document metadata and cached artefacts for a payment. // DocumentRecord stores document metadata and cached artefacts for a payment.
type DocumentRecord struct { type DocumentRecord struct {
storable.Base `bson:",inline" json:",inline"` storable.Base `bson:",inline" json:",inline"`
PaymentRef string `bson:"paymentRef" json:"paymentRef"`
PaymentRef string `bson:"paymentRef" json:"paymentRef"` Snapshot ActSnapshot `bson:"snapshot" json:"snapshot"`
Snapshot ActSnapshot `bson:"snapshot" json:"snapshot"` StoragePaths map[DocumentType]string `bson:"storagePaths,omitempty" json:"storagePaths,omitempty"`
StoragePaths map[DocumentType]string `bson:"storagePaths,omitempty" json:"storagePaths,omitempty"` Hashes map[DocumentType]string `bson:"hashes,omitempty" json:"hashes,omitempty"`
Hashes map[DocumentType]string `bson:"hashes,omitempty" json:"hashes,omitempty"`
} }
func (r *DocumentRecord) Normalize() { func (r *DocumentRecord) Normalize() {
if r == nil { if r == nil {
return return
} }
r.PaymentRef = strings.TrimSpace(r.PaymentRef) r.PaymentRef = strings.TrimSpace(r.PaymentRef)
r.Snapshot.Normalize() r.Snapshot.Normalize()
if r.StoragePaths == nil { if r.StoragePaths == nil {
r.StoragePaths = map[DocumentType]string{} r.StoragePaths = map[DocumentType]string{}
} }
if r.Hashes == nil { if r.Hashes == nil {
r.Hashes = map[DocumentType]string{} r.Hashes = map[DocumentType]string{}
} }

View File

@@ -42,22 +42,18 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
defer cancel() defer cancel()
if err := result.Ping(ctx); err != nil { if err := result.Ping(ctx); err != nil {
result.logger.Error("Mongo ping failed during store init", zap.Error(err)) result.logger.Error("mongo ping failed during store init", zap.Error(err))
return nil, err return nil, err
} }
documentsStore, err := store.NewDocuments(result.logger, database) documentsStore, err := store.NewDocuments(result.logger, database)
if err != nil { if err != nil {
result.logger.Error("Failed to initialise documents store", zap.Error(err)) result.logger.Error("failed to initialise documents store", zap.Error(err))
return nil, err return nil, err
} }
result.documents = documentsStore result.documents = documentsStore
result.logger.Info("Billing documents MongoDB storage initialised") result.logger.Info("Billing documents MongoDB storage initialised")
return result, nil return result, nil
} }

View File

@@ -38,14 +38,13 @@ func NewDocuments(logger mlogger.Logger, db *mongo.Database) (*Documents, error)
for _, def := range indexes { for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil { if err := repo.CreateIndex(def); err != nil {
logger.Error("Failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection())) logger.Error("failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err return nil, err
} }
} }
childLogger := logger.Named("documents") childLogger := logger.Named("documents")
childLogger.Debug("Documents store initialised") childLogger.Debug("documents store initialised")
return &Documents{ return &Documents{
logger: childLogger, logger: childLogger,
@@ -57,9 +56,7 @@ func (d *Documents) Create(ctx context.Context, record *model.DocumentRecord) er
if record == nil { if record == nil {
return merrors.InvalidArgument("documentsStore: nil record") return merrors.InvalidArgument("documentsStore: nil record")
} }
record.Normalize() record.Normalize()
if record.PaymentRef == "" { if record.PaymentRef == "" {
return merrors.InvalidArgument("documentsStore: empty paymentRef") return merrors.InvalidArgument("documentsStore: empty paymentRef")
} }
@@ -69,12 +66,9 @@ func (d *Documents) Create(ctx context.Context, record *model.DocumentRecord) er
if errors.Is(err, merrors.ErrDataConflict) { if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicateDocument return storage.ErrDuplicateDocument
} }
return err return err
} }
d.logger.Debug("document record created", zap.String("payment_ref", record.PaymentRef))
d.logger.Debug("Document record created", zap.String("payment_ref", record.PaymentRef))
return nil return nil
} }
@@ -82,21 +76,17 @@ func (d *Documents) Update(ctx context.Context, record *model.DocumentRecord) er
if record == nil { if record == nil {
return merrors.InvalidArgument("documentsStore: nil record") return merrors.InvalidArgument("documentsStore: nil record")
} }
if record.ID.IsZero() { if record.ID.IsZero() {
return merrors.InvalidArgument("documentsStore: missing record id") return merrors.InvalidArgument("documentsStore: missing record id")
} }
record.Normalize() record.Normalize()
record.Update() record.Update()
if err := d.repo.Update(ctx, record); err != nil { if err := d.repo.Update(ctx, record); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return storage.ErrDocumentNotFound return storage.ErrDocumentNotFound
} }
return err return err
} }
return nil return nil
} }
@@ -111,10 +101,8 @@ func (d *Documents) GetByPaymentRef(ctx context.Context, paymentRef string) (*mo
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrDocumentNotFound return nil, storage.ErrDocumentNotFound
} }
return nil, err return nil, err
} }
return entity, nil return entity, nil
} }
@@ -125,34 +113,26 @@ func (d *Documents) ListByPaymentRefs(ctx context.Context, paymentRefs []string)
if clean == "" { if clean == "" {
continue continue
} }
refs = append(refs, clean) refs = append(refs, clean)
} }
if len(refs) == 0 { if len(refs) == 0 {
return []*model.DocumentRecord{}, nil return []*model.DocumentRecord{}, nil
} }
query := repository.Query().Comparison(repository.Field("paymentRef"), builder.In, refs) query := repository.Query().Comparison(repository.Field("paymentRef"), builder.In, refs)
records := make([]*model.DocumentRecord, 0) records := make([]*model.DocumentRecord, 0)
decoder := func(cur *mongo.Cursor) error { decoder := func(cur *mongo.Cursor) error {
var rec model.DocumentRecord var rec model.DocumentRecord
if err := cur.Decode(&rec); err != nil { if err := cur.Decode(&rec); err != nil {
d.logger.Warn("Failed to decode document record", zap.Error(err)) d.logger.Warn("failed to decode document record", zap.Error(err))
return err return err
} }
records = append(records, &rec) records = append(records, &rec)
return nil return nil
} }
if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil { if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil {
return nil, err return nil, err
} }
return records, nil return records, nil
} }

View File

@@ -1,198 +0,0 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- noinlineerr
- wsl
- wrapcheck
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- cyclop
- funlen
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/billing/fees module github.com/tech/sendico/billing/fees
go 1.25.7 go 1.25.6
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
@@ -10,7 +10,7 @@ require (
github.com/tech/sendico/fx/oracle v0.0.0 github.com/tech/sendico/fx/oracle v0.0.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.78.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -25,31 +25,31 @@ require (
github.com/casbin/casbin/v2 v2.135.0 // indirect github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-chi/chi/v5 v5.2.4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
) )

View File

@@ -38,8 +38,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -152,16 +152,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/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 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -191,16 +191,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,10 +208,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -24,6 +24,5 @@ func Create() version.Printer {
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&info) return vf.Create(&info)
} }

View File

@@ -31,8 +31,7 @@ type Imp struct {
type config struct { type config struct {
*grpcapp.Config `yaml:",inline"` *grpcapp.Config `yaml:",inline"`
Oracle OracleConfig `yaml:"oracle"`
Oracle OracleConfig `yaml:"oracle"`
} }
type OracleConfig struct { type OracleConfig struct {
@@ -46,7 +45,6 @@ func (c OracleConfig) dialTimeout() time.Duration {
if c.DialTimeoutSecs <= 0 { if c.DialTimeoutSecs <= 0 {
return 5 * time.Second return 5 * time.Second
} }
return time.Duration(c.DialTimeoutSecs) * time.Second return time.Duration(c.DialTimeoutSecs) * time.Second
} }
@@ -54,7 +52,6 @@ func (c OracleConfig) callTimeout() time.Duration {
if c.CallTimeoutSecs <= 0 { if c.CallTimeoutSecs <= 0 {
return 3 * time.Second return 3 * time.Second
} }
return time.Duration(c.CallTimeoutSecs) * time.Second return time.Duration(c.CallTimeoutSecs) * time.Second
} }
@@ -72,11 +69,9 @@ func (i *Imp) Shutdown() {
if i.service != nil { if i.service != nil {
i.service.Shutdown() i.service.Shutdown()
} }
if i.oracleClient != nil { if i.oracleClient != nil {
_ = i.oracleClient.Close() _ = i.oracleClient.Close()
} }
return return
} }
@@ -103,7 +98,6 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.config = cfg i.config = cfg
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
@@ -111,23 +105,22 @@ func (i *Imp) Start() error {
} }
var oracleClient oracleclient.Client var oracleClient oracleclient.Client
if addr := strings.TrimSpace(cfg.Oracle.Address); addr != "" { if addr := strings.TrimSpace(cfg.Oracle.Address); addr != "" {
dialCtx, cancel := context.WithTimeout(context.Background(), cfg.Oracle.dialTimeout()) dialCtx, cancel := context.WithTimeout(context.Background(), cfg.Oracle.dialTimeout())
defer cancel() defer cancel()
oracleConn, err := oracleclient.New(dialCtx, oracleclient.Config{ oc, err := oracleclient.New(dialCtx, oracleclient.Config{
Address: addr, Address: addr,
DialTimeout: cfg.Oracle.dialTimeout(), DialTimeout: cfg.Oracle.dialTimeout(),
CallTimeout: cfg.Oracle.callTimeout(), CallTimeout: cfg.Oracle.callTimeout(),
Insecure: cfg.Oracle.InsecureTransport, Insecure: cfg.Oracle.InsecureTransport,
}) })
if err != nil { if err != nil {
i.logger.Warn("Failed to initialise oracle client", zap.String("address", addr), zap.Error(err)) i.logger.Warn("failed to initialise oracle client", zap.String("address", addr), zap.Error(err))
} else { } else {
oracleClient = oracleConn oracleClient = oc
i.oracleClient = oracleConn i.oracleClient = oc
i.logger.Info("Connected to oracle service", zap.String("address", addr)) i.logger.Info("connected to oracle service", zap.String("address", addr))
} }
} }
@@ -136,16 +129,13 @@ func (i *Imp) Start() error {
if oracleClient != nil { if oracleClient != nil {
opts = append(opts, fees.WithOracleClient(oracleClient)) opts = append(opts, fees.WithOracleClient(oracleClient))
} }
if cfg.GRPC != nil { if cfg.GRPC != nil {
if invokeURI := cfg.GRPC.DiscoveryInvokeURI(); invokeURI != "" { if invokeURI := cfg.GRPC.DiscoveryInvokeURI(); invokeURI != "" {
opts = append(opts, fees.WithDiscoveryInvokeURI(invokeURI)) opts = append(opts, fees.WithDiscoveryInvokeURI(invokeURI))
} }
} }
svc := fees.NewService(logger, repo, producer, opts...) svc := fees.NewService(logger, repo, producer, opts...)
i.service = svc i.service = svc
return svc, nil return svc, nil
} }
@@ -153,7 +143,6 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.app = app i.app = app
return i.app.Start() return i.app.Start()
@@ -163,14 +152,12 @@ func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err return nil, err
} }
cfg := &config{Config: &grpcapp.Config{}} cfg := &config{Config: &grpcapp.Config{}}
if err := yaml.Unmarshal(data, cfg); err != nil { if err := yaml.Unmarshal(data, cfg); err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err return nil, err
} }

View File

@@ -3,7 +3,6 @@ package calculator
import ( import (
"context" "context"
"errors" "errors"
"maps"
"math/big" "math/big"
"sort" "sort"
"strconv" "strconv"
@@ -34,7 +33,6 @@ func New(logger mlogger.Logger, oracle fxOracle) *quoteCalculator {
if logger == nil { if logger == nil {
logger = zap.NewNop() logger = zap.NewNop()
} }
return &quoteCalculator{ return &quoteCalculator{
logger: logger.Named("calculator"), logger: logger.Named("calculator"),
oracle: oracle, oracle: oracle,
@@ -47,10 +45,27 @@ type quoteCalculator struct {
} }
func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, intent *feesv1.Intent, bookedAt time.Time, _ *tracev1.TraceContext) (*types.CalculationResult, error) { func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, intent *feesv1.Intent, bookedAt time.Time, _ *tracev1.TraceContext) (*types.CalculationResult, error) {
baseAmount, baseScale, trigger, err := validateComputeInputs(plan, intent) if plan == nil {
if err != nil { return nil, merrors.InvalidArgument("plan is required")
return nil, err
} }
if intent == nil {
return nil, merrors.InvalidArgument("intent is required")
}
trigger := convertTrigger(intent.GetTrigger())
if trigger == model.TriggerUnspecified {
return nil, merrors.InvalidArgument("unsupported trigger")
}
baseAmount, err := dmath.RatFromString(intent.GetBaseAmount().GetAmount())
if err != nil {
return nil, merrors.InvalidArgument("invalid base amount")
}
if baseAmount.Sign() < 0 {
return nil, merrors.InvalidArgument("base amount cannot be negative")
}
baseScale := inferScale(intent.GetBaseAmount().GetAmount())
rules := make([]model.FeeRule, len(plan.Rules)) rules := make([]model.FeeRule, len(plan.Rules))
copy(rules, plan.Rules) copy(rules, plan.Rules)
@@ -58,37 +73,81 @@ func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, inte
if rules[i].Priority == rules[j].Priority { if rules[i].Priority == rules[j].Priority {
return rules[i].RuleID < rules[j].RuleID return rules[i].RuleID < rules[j].RuleID
} }
return rules[i].Priority < rules[j].Priority return rules[i].Priority < rules[j].Priority
}) })
planID := planIDFrom(plan)
lines := make([]*feesv1.DerivedPostingLine, 0, len(rules)) lines := make([]*feesv1.DerivedPostingLine, 0, len(rules))
applied := make([]*feesv1.AppliedRule, 0, len(rules)) applied := make([]*feesv1.AppliedRule, 0, len(rules))
planID := ""
if planRef := plan.GetID(); planRef != nil && !planRef.IsZero() {
planID = planRef.Hex()
}
for _, rule := range rules { for _, rule := range rules {
if !shouldApplyRule(rule, trigger, intent.GetAttributes(), bookedAt) { if !shouldApplyRule(rule, trigger, intent.GetAttributes(), bookedAt) {
continue continue
} }
ledgerAccountRef := strings.TrimSpace(rule.LedgerAccountRef)
amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule) amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule)
if calcErr != nil { if calcErr != nil {
if !errors.Is(calcErr, merrors.ErrInvalidArg) { if !errors.Is(calcErr, merrors.ErrInvalidArg) {
c.logger.Warn("Failed to calculate fee rule amount", zap.String("rule_id", rule.RuleID), zap.Error(calcErr)) c.logger.Warn("failed to calculate fee rule amount", zap.String("rule_id", rule.RuleID), zap.Error(calcErr))
} }
continue continue
} }
if amount.Sign() == 0 { if amount.Sign() == 0 {
continue continue
} }
currency := resolvedCurrency(intent.GetBaseAmount().GetCurrency(), rule.Currency) currency := intent.GetBaseAmount().GetCurrency()
entrySide := resolvedEntrySide(rule) if override := strings.TrimSpace(rule.Currency); override != "" {
currency = override
}
lines = append(lines, buildPostingLine(rule, amount, scale, currency, entrySide, planID)) entrySide := mapEntrySide(rule.EntrySide)
applied = append(applied, buildAppliedRule(rule, planID)) if entrySide == accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED {
// Default fees to debit (i.e. charge the customer) when entry side is not specified.
entrySide = accountingv1.EntrySide_ENTRY_SIDE_DEBIT
}
meta := map[string]string{
"fee_rule_id": rule.RuleID,
}
if planID != "" {
meta["fee_plan_id"] = planID
}
if rule.Metadata != nil {
if taxCode := strings.TrimSpace(rule.Metadata["tax_code"]); taxCode != "" {
meta["tax_code"] = taxCode
}
if taxRate := strings.TrimSpace(rule.Metadata["tax_rate"]); taxRate != "" {
meta["tax_rate"] = taxRate
}
}
lines = append(lines, &feesv1.DerivedPostingLine{
LedgerAccountRef: ledgerAccountRef,
Money: &moneyv1.Money{
Amount: dmath.FormatRat(amount, scale),
Currency: currency,
},
LineType: mapLineType(rule.LineType),
Side: entrySide,
Meta: meta,
})
applied = append(applied, &feesv1.AppliedRule{
RuleId: rule.RuleID,
RuleVersion: planID,
Formula: rule.Formula,
Rounding: mapRoundingMode(rule.Rounding),
TaxCode: metadataValue(rule.Metadata, "tax_code"),
TaxRate: metadataValue(rule.Metadata, "tax_rate"),
Parameters: cloneStringMap(rule.Metadata),
})
} }
var fxUsed *feesv1.FXUsed var fxUsed *feesv1.FXUsed
@@ -111,24 +170,40 @@ func (c *quoteCalculator) calculateRuleAmount(baseAmount *big.Rat, baseScale uin
result := new(big.Rat) result := new(big.Rat)
result, err = applyPercentage(result, baseAmount, rule.Percentage) if percentage := strings.TrimSpace(rule.Percentage); percentage != "" {
if err != nil { percentageRat, perr := dmath.RatFromString(percentage)
return nil, 0, err if perr != nil {
return nil, 0, merrors.InvalidArgument("invalid percentage")
}
result = dmath.AddRat(result, dmath.MulRat(baseAmount, percentageRat))
} }
result, err = applyFixed(result, rule.FixedAmount) if fixed := strings.TrimSpace(rule.FixedAmount); fixed != "" {
if err != nil { fixedRat, ferr := dmath.RatFromString(fixed)
return nil, 0, err if ferr != nil {
return nil, 0, merrors.InvalidArgument("invalid fixed amount")
}
result = dmath.AddRat(result, fixedRat)
} }
result, err = applyMin(result, rule.MinimumAmount) if minStr := strings.TrimSpace(rule.MinimumAmount); minStr != "" {
if err != nil { minRat, merr := dmath.RatFromString(minStr)
return nil, 0, err if merr != nil {
return nil, 0, merrors.InvalidArgument("invalid minimum amount")
}
if dmath.CmpRat(result, minRat) < 0 {
result = new(big.Rat).Set(minRat)
}
} }
result, err = applyMax(result, rule.MaximumAmount) if maxStr := strings.TrimSpace(rule.MaximumAmount); maxStr != "" {
if err != nil { maxRat, merr := dmath.RatFromString(maxStr)
return nil, 0, err if merr != nil {
return nil, 0, merrors.InvalidArgument("invalid maximum amount")
}
if dmath.CmpRat(result, maxRat) > 0 {
result = new(big.Rat).Set(maxRat)
}
} }
if result.Sign() < 0 { if result.Sign() < 0 {
@@ -143,66 +218,6 @@ func (c *quoteCalculator) calculateRuleAmount(baseAmount *big.Rat, baseScale uin
return rounded, scale, nil return rounded, scale, nil
} }
func applyPercentage(result, baseAmount *big.Rat, percentage string) (*big.Rat, error) {
if strings.TrimSpace(percentage) == "" {
return result, nil
}
percentageRat, err := dmath.RatFromString(percentage)
if err != nil {
return nil, merrors.InvalidArgument("invalid percentage")
}
return dmath.AddRat(result, dmath.MulRat(baseAmount, percentageRat)), nil
}
func applyFixed(result *big.Rat, fixed string) (*big.Rat, error) {
if strings.TrimSpace(fixed) == "" {
return result, nil
}
fixedRat, err := dmath.RatFromString(fixed)
if err != nil {
return nil, merrors.InvalidArgument("invalid fixed amount")
}
return dmath.AddRat(result, fixedRat), nil
}
func applyMin(result *big.Rat, minStr string) (*big.Rat, error) {
if strings.TrimSpace(minStr) == "" {
return result, nil
}
minRat, err := dmath.RatFromString(minStr)
if err != nil {
return nil, merrors.InvalidArgument("invalid minimum amount")
}
if dmath.CmpRat(result, minRat) < 0 {
return new(big.Rat).Set(minRat), nil
}
return result, nil
}
func applyMax(result *big.Rat, maxStr string) (*big.Rat, error) {
if strings.TrimSpace(maxStr) == "" {
return result, nil
}
maxRat, err := dmath.RatFromString(maxStr)
if err != nil {
return nil, merrors.InvalidArgument("invalid maximum amount")
}
if dmath.CmpRat(result, maxRat) > 0 {
return new(big.Rat).Set(maxRat), nil
}
return result, nil
}
const ( const (
attrFxBaseCurrency = "fx_base_currency" attrFxBaseCurrency = "fx_base_currency"
attrFxQuoteCurrency = "fx_quote_currency" attrFxQuoteCurrency = "fx_quote_currency"
@@ -217,9 +232,7 @@ func (c *quoteCalculator) buildFxUsed(ctx context.Context, intent *feesv1.Intent
} }
attrs := intent.GetAttributes() attrs := intent.GetAttributes()
base := strings.TrimSpace(attrs[attrFxBaseCurrency]) base := strings.TrimSpace(attrs[attrFxBaseCurrency])
quote := strings.TrimSpace(attrs[attrFxQuoteCurrency]) quote := strings.TrimSpace(attrs[attrFxQuoteCurrency])
if base == "" || quote == "" { if base == "" || quote == "" {
return nil return nil
@@ -234,26 +247,20 @@ func (c *quoteCalculator) buildFxUsed(ctx context.Context, intent *feesv1.Intent
Provider: provider, Provider: provider,
}) })
if err != nil { if err != nil {
c.logger.Warn("Fees: failed to fetch FX context", zap.Error(err)) c.logger.Warn("fees: failed to fetch FX context", zap.Error(err))
return nil return nil
} }
if snapshot == nil { if snapshot == nil {
return nil return nil
} }
rateValue := strings.TrimSpace(attrs[attrFxRateOverride]) rateValue := strings.TrimSpace(attrs[attrFxRateOverride])
if rateValue == "" { if rateValue == "" {
rateValue = snapshot.Mid rateValue = snapshot.Mid
} }
if rateValue == "" { if rateValue == "" {
rateValue = snapshot.Ask rateValue = snapshot.Ask
} }
if rateValue == "" { if rateValue == "" {
rateValue = snapshot.Bid rateValue = snapshot.Bid
} }
@@ -285,19 +292,15 @@ func inferScale(amount string) uint32 {
if value == "" { if value == "" {
return 0 return 0
} }
if idx := strings.IndexAny(value, "eE"); idx >= 0 { if idx := strings.IndexAny(value, "eE"); idx >= 0 {
value = value[:idx] value = value[:idx]
} }
if strings.HasPrefix(value, "+") || strings.HasPrefix(value, "-") { if strings.HasPrefix(value, "+") || strings.HasPrefix(value, "-") {
value = value[1:] value = value[1:]
} }
if dot := strings.IndexByte(value, '.'); dot >= 0 {
if _, after, found := strings.Cut(value, "."); found { return uint32(len(value[dot+1:]))
return uint32(len(after)) //nolint:gosec // decimal scale; cannot overflow
} }
return 0 return 0
} }
@@ -305,15 +308,12 @@ func shouldApplyRule(rule model.FeeRule, trigger model.Trigger, attributes map[s
if rule.Trigger != trigger { if rule.Trigger != trigger {
return false return false
} }
if rule.EffectiveFrom.After(bookedAt) { if rule.EffectiveFrom.After(bookedAt) {
return false return false
} }
if rule.EffectiveTo != nil && rule.EffectiveTo.Before(bookedAt) { if rule.EffectiveTo != nil && rule.EffectiveTo.Before(bookedAt) {
return false return false
} }
return ruleMatchesAttributes(rule, attributes) return ruleMatchesAttributes(rule, attributes)
} }
@@ -325,7 +325,6 @@ func resolveRuleScale(rule model.FeeRule, fallback uint32) (uint32, error) {
} }
} }
} }
return fallback, nil return fallback, nil
} }
@@ -334,115 +333,17 @@ func parseScale(field, value string) (uint32, error) {
if clean == "" { if clean == "" {
return 0, merrors.InvalidArgument(field + " is empty") return 0, merrors.InvalidArgument(field + " is empty")
} }
parsed, err := strconv.ParseUint(clean, 10, 32) parsed, err := strconv.ParseUint(clean, 10, 32)
if err != nil { if err != nil {
return 0, merrors.InvalidArgument("invalid " + field + " value") return 0, merrors.InvalidArgument("invalid " + field + " value")
} }
return uint32(parsed), nil return uint32(parsed), nil
} }
func validateComputeInputs(plan *model.FeePlan, intent *feesv1.Intent) (*big.Rat, uint32, model.Trigger, error) {
if plan == nil {
return nil, 0, model.TriggerUnspecified, merrors.InvalidArgument("plan is required")
}
if intent == nil {
return nil, 0, model.TriggerUnspecified, merrors.InvalidArgument("intent is required")
}
trigger := convertTrigger(intent.GetTrigger())
if trigger == model.TriggerUnspecified {
return nil, 0, model.TriggerUnspecified, merrors.InvalidArgument("unsupported trigger")
}
baseAmount, err := dmath.RatFromString(intent.GetBaseAmount().GetAmount())
if err != nil {
return nil, 0, trigger, merrors.InvalidArgument("invalid base amount")
}
if baseAmount.Sign() < 0 {
return nil, 0, trigger, merrors.InvalidArgument("base amount cannot be negative")
}
return baseAmount, inferScale(intent.GetBaseAmount().GetAmount()), trigger, nil
}
func planIDFrom(plan *model.FeePlan) string {
if planRef := plan.GetID(); planRef != nil && !planRef.IsZero() {
return planRef.Hex()
}
return ""
}
func resolvedCurrency(baseCurrency, ruleCurrency string) string {
if override := strings.TrimSpace(ruleCurrency); override != "" {
return override
}
return baseCurrency
}
func resolvedEntrySide(rule model.FeeRule) accountingv1.EntrySide {
// Default fees to debit (i.e. charge the customer) when entry side is not specified.
if side := mapEntrySide(rule.EntrySide); side != accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED {
return side
}
return accountingv1.EntrySide_ENTRY_SIDE_DEBIT
}
func buildPostingLine(rule model.FeeRule, amount *big.Rat, scale uint32, currency string, entrySide accountingv1.EntrySide, planID string) *feesv1.DerivedPostingLine {
return &feesv1.DerivedPostingLine{
LedgerAccountRef: strings.TrimSpace(rule.LedgerAccountRef),
Money: &moneyv1.Money{
Amount: dmath.FormatRat(amount, scale),
Currency: currency,
},
LineType: mapLineType(rule.LineType),
Side: entrySide,
Meta: buildRuleMeta(rule, planID),
}
}
func buildAppliedRule(rule model.FeeRule, planID string) *feesv1.AppliedRule {
return &feesv1.AppliedRule{
RuleId: rule.RuleID,
RuleVersion: planID,
Formula: rule.Formula,
Rounding: mapRoundingMode(rule.Rounding),
TaxCode: metadataValue(rule.Metadata, "tax_code"),
TaxRate: metadataValue(rule.Metadata, "tax_rate"),
Parameters: cloneStringMap(rule.Metadata),
}
}
func buildRuleMeta(rule model.FeeRule, planID string) map[string]string {
meta := map[string]string{"fee_rule_id": rule.RuleID}
if planID != "" {
meta["fee_plan_id"] = planID
}
if rule.Metadata != nil {
if taxCode := strings.TrimSpace(rule.Metadata["tax_code"]); taxCode != "" {
meta["tax_code"] = taxCode
}
if taxRate := strings.TrimSpace(rule.Metadata["tax_rate"]); taxRate != "" {
meta["tax_rate"] = taxRate
}
}
return meta
}
func metadataValue(meta map[string]string, key string) string { func metadataValue(meta map[string]string, key string) string {
if meta == nil { if meta == nil {
return "" return ""
} }
return strings.TrimSpace(meta[key]) return strings.TrimSpace(meta[key])
} }
@@ -450,10 +351,10 @@ func cloneStringMap(src map[string]string) map[string]string {
if len(src) == 0 { if len(src) == 0 {
return nil return nil
} }
cloned := make(map[string]string, len(src)) cloned := make(map[string]string, len(src))
maps.Copy(cloned, src) for k, v := range src {
cloned[k] = v
}
return cloned return cloned
} }
@@ -461,22 +362,18 @@ func ruleMatchesAttributes(rule model.FeeRule, attributes map[string]string) boo
if len(rule.AppliesTo) == 0 { if len(rule.AppliesTo) == 0 {
return true return true
} }
for key, value := range rule.AppliesTo { for key, value := range rule.AppliesTo {
if attributes == nil { if attributes == nil {
return false return false
} }
attrValue, ok := attributes[key] attrValue, ok := attributes[key]
if !ok { if !ok {
return false return false
} }
if !matchesAttributeValue(value, attrValue) { if !matchesAttributeValue(value, attrValue) {
return false return false
} }
} }
return true return true
} }
@@ -486,17 +383,16 @@ func matchesAttributeValue(expected, actual string) bool {
return actual == "" return actual == ""
} }
for value := range strings.SplitSeq(trimmed, ",") { values := strings.Split(trimmed, ",")
for _, value := range values {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
continue continue
} }
if value == "*" || value == actual { if value == "*" || value == actual {
return true return true
} }
} }
return false return false
} }
@@ -550,8 +446,6 @@ func mapRoundingMode(mode string) moneyv1.RoundingMode {
func convertTrigger(trigger feesv1.Trigger) model.Trigger { func convertTrigger(trigger feesv1.Trigger) model.Trigger {
switch trigger { switch trigger {
case feesv1.Trigger_TRIGGER_UNSPECIFIED:
return model.TriggerUnspecified
case feesv1.Trigger_TRIGGER_CAPTURE: case feesv1.Trigger_TRIGGER_CAPTURE:
return model.TriggerCapture return model.TriggerCapture
case feesv1.Trigger_TRIGGER_REFUND: case feesv1.Trigger_TRIGGER_REFUND:

View File

@@ -9,7 +9,6 @@ import (
"github.com/tech/sendico/billing/fees/storage" "github.com/tech/sendico/billing/fees/storage"
"github.com/tech/sendico/billing/fees/storage/model" "github.com/tech/sendico/billing/fees/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap" "github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
@@ -23,19 +22,17 @@ type planFinder interface {
type feeResolver struct { type feeResolver struct {
plans storage.PlansStore plans storage.PlansStore
finder planFinder finder planFinder
logger mlogger.Logger logger *zap.Logger
} }
func New(plans storage.PlansStore, logger mlogger.Logger) *feeResolver { func New(plans storage.PlansStore, logger *zap.Logger) *feeResolver {
var finder planFinder var finder planFinder
if pf, ok := plans.(planFinder); ok { if pf, ok := plans.(planFinder); ok {
finder = pf finder = pf
} }
if logger == nil { if logger == nil {
logger = zap.NewNop() logger = zap.NewNop()
} }
return &feeResolver{ return &feeResolver{
plans: plans, plans: plans,
finder: finder, finder: finder,
@@ -43,100 +40,63 @@ func New(plans storage.PlansStore, logger mlogger.Logger) *feeResolver {
} }
} }
func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *bson.ObjectID, trigger model.Trigger, asOf time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) { func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *bson.ObjectID, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) {
if r.plans == nil { if r.plans == nil {
return nil, nil, merrors.InvalidArgument("fees: plans store is required") return nil, nil, merrors.InvalidArgument("fees: plans store is required")
} }
// Try org-specific first if provided. // Try org-specific first if provided.
if isOrgRef(orgRef) { if orgRef != nil && !orgRef.IsZero() {
plan, rule, err := r.tryOrgRule(ctx, *orgRef, trigger, asOf, attrs) if plan, err := r.getOrgPlan(ctx, *orgRef, at); err == nil {
if err != nil { if rule, selErr := selectRule(plan, trigger, at, attrs); selErr == nil {
return plan, rule, nil
} else if !errors.Is(selErr, ErrNoFeeRuleFound) {
r.logger.Warn("Failed selecting rule for org plan", zap.Error(selErr), mzap.ObjRef("org_ref", *orgRef))
return nil, nil, selErr
}
orgFields := []zap.Field{
mzap.ObjRef("org_ref", *orgRef),
zap.String("trigger", string(trigger)),
zap.Time("booked_at", at),
zap.Any("attributes", attrs),
}
orgFields = append(orgFields, zapFieldsForPlan(plan)...)
r.logger.Debug("No matching rule in org plan; falling back to global", orgFields...)
} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
r.logger.Warn("Failed resolving org fee plan", zap.Error(err), mzap.ObjRef("org_ref", *orgRef))
return nil, nil, err return nil, nil, err
} }
if rule != nil {
return plan, rule, nil
}
} }
plan, err := r.getGlobalPlan(ctx, asOf) plan, err := r.getGlobalPlan(ctx, at)
if err != nil { if err != nil {
if errors.Is(err, storage.ErrFeePlanNotFound) { if errors.Is(err, storage.ErrFeePlanNotFound) {
r.logger.Debug("No applicable global fee plan found", zap.String("trigger", string(trigger)), r.logger.Debug("No applicable global fee plan found", zap.String("trigger", string(trigger)),
zap.Time("booked_at", asOf), zap.Any("attributes", attrs), zap.Time("booked_at", at), zap.Any("attributes", attrs),
) )
return nil, nil, merrors.NoData("fees: no applicable fee rule found") return nil, nil, merrors.NoData("fees: no applicable fee rule found")
} }
r.logger.Warn("Failed resolving global fee plan", zap.Error(err)) r.logger.Warn("Failed resolving global fee plan", zap.Error(err))
return nil, nil, err return nil, nil, err
} }
rule, err := selectRule(plan, trigger, asOf, attrs) rule, err := selectRule(plan, trigger, at, attrs)
if err != nil { if err != nil {
if !errors.Is(err, ErrNoFeeRuleFound) { if !errors.Is(err, ErrNoFeeRuleFound) {
r.logger.Warn("Failed selecting rule in global plan", zap.Error(err)) r.logger.Warn("Failed selecting rule in global plan", zap.Error(err))
} else { } else {
globalFields := zapFieldsForPlan(plan) globalFields := []zap.Field{
globalFields = append([]zap.Field{
zap.String("trigger", string(trigger)), zap.String("trigger", string(trigger)),
zap.Time("booked_at", asOf), zap.Time("booked_at", at),
zap.Any("attributes", attrs), zap.Any("attributes", attrs),
}, globalFields...) }
globalFields = append(globalFields, zapFieldsForPlan(plan)...)
r.logger.Debug("No matching rule in global plan", globalFields...) r.logger.Debug("No matching rule in global plan", globalFields...)
} }
return nil, nil, err return nil, nil, err
} }
r.logSelectedRule(orgRef, trigger, asOf, attrs, rule, plan) selectedFields := []zap.Field{
return plan, rule, nil
}
// tryOrgRule attempts to find a matching rule in the org-specific plan.
// Returns (plan, rule, nil) on success, (nil, nil, nil) to signal fallthrough to global,
// or (nil, nil, err) on a hard error.
func (r *feeResolver) tryOrgRule(ctx context.Context, orgRef bson.ObjectID, trigger model.Trigger, asOf time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) {
plan, err := r.getOrgPlan(ctx, orgRef, asOf)
if err != nil {
if errors.Is(err, storage.ErrFeePlanNotFound) {
return nil, nil, nil
}
r.logger.Warn("Failed resolving org fee plan", zap.Error(err), mzap.ObjRef("org_ref", orgRef))
return nil, nil, err
}
rule, selErr := selectRule(plan, trigger, asOf, attrs)
if selErr == nil {
return plan, rule, nil
}
if !errors.Is(selErr, ErrNoFeeRuleFound) {
r.logger.Warn("Failed selecting rule for org plan", zap.Error(selErr), mzap.ObjRef("org_ref", orgRef))
return nil, nil, selErr
}
orgFields := zapFieldsForPlan(plan)
orgFields = append([]zap.Field{
mzap.ObjRef("org_ref", orgRef),
zap.String("trigger", string(trigger)),
zap.Time("booked_at", asOf),
zap.Any("attributes", attrs),
}, orgFields...)
r.logger.Debug("No matching rule in org plan; falling back to global", orgFields...)
return nil, nil, nil
}
func (r *feeResolver) logSelectedRule(orgRef *bson.ObjectID, trigger model.Trigger, at time.Time, attrs map[string]string, rule *model.FeeRule, plan *model.FeePlan) {
fields := []zap.Field{
zap.String("trigger", string(trigger)), zap.String("trigger", string(trigger)),
zap.Time("booked_at", at), zap.Time("booked_at", at),
zap.Any("attributes", attrs), zap.Any("attributes", attrs),
@@ -146,65 +106,59 @@ func (r *feeResolver) logSelectedRule(orgRef *bson.ObjectID, trigger model.Trigg
zap.Time("rule_effective_from", rule.EffectiveFrom), zap.Time("rule_effective_from", rule.EffectiveFrom),
} }
if rule.EffectiveTo != nil { if rule.EffectiveTo != nil {
fields = append(fields, zap.Time("rule_effective_to", *rule.EffectiveTo)) selectedFields = append(selectedFields, zap.Time("rule_effective_to", *rule.EffectiveTo))
} }
if orgRef != nil && !orgRef.IsZero() {
if isOrgRef(orgRef) { selectedFields = append(selectedFields, mzap.ObjRef("org_ref", *orgRef))
fields = append(fields, mzap.ObjRef("org_ref", *orgRef))
} }
selectedFields = append(selectedFields, zapFieldsForPlan(plan)...)
r.logger.Debug("Selected fee rule", selectedFields...)
fields = append(fields, zapFieldsForPlan(plan)...) return plan, rule, nil
r.logger.Debug("Selected fee rule", fields...)
}
func isOrgRef(ref *bson.ObjectID) bool {
return ref != nil && !ref.IsZero()
} }
func (r *feeResolver) getOrgPlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (r *feeResolver) getOrgPlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
if r.finder != nil { if r.finder != nil {
return r.finder.FindActiveOrgPlan(ctx, orgRef, at) return r.finder.FindActiveOrgPlan(ctx, orgRef, at)
} }
return r.plans.GetActivePlan(ctx, orgRef, at) return r.plans.GetActivePlan(ctx, orgRef, at)
} }
func (r *feeResolver) getGlobalPlan(ctx context.Context, asOf time.Time) (*model.FeePlan, error) { func (r *feeResolver) getGlobalPlan(ctx context.Context, at time.Time) (*model.FeePlan, error) {
if r.finder != nil { if r.finder != nil {
return r.finder.FindActiveGlobalPlan(ctx, asOf) return r.finder.FindActiveGlobalPlan(ctx, at)
} }
// Treat zero ObjectID as global in legacy path. // Treat zero ObjectID as global in legacy path.
return r.plans.GetActivePlan(ctx, bson.NilObjectID, asOf) return r.plans.GetActivePlan(ctx, bson.NilObjectID, at)
} }
func selectRule(plan *model.FeePlan, trigger model.Trigger, asOf time.Time, attrs map[string]string) (*model.FeeRule, error) { func selectRule(plan *model.FeePlan, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeeRule, error) {
if plan == nil { if plan == nil {
return nil, merrors.NoData("fees: no applicable fee rule found") return nil, merrors.NoData("fees: no applicable fee rule found")
} }
var ( var selected *model.FeeRule
selected *model.FeeRule var highestPriority int
highestPriority int
)
for _, rule := range plan.Rules { for _, rule := range plan.Rules {
if !ruleIsActive(rule, trigger, asOf) { if rule.Trigger != trigger {
continue
}
if rule.EffectiveFrom.After(at) {
continue
}
if rule.EffectiveTo != nil && !rule.EffectiveTo.After(at) {
continue continue
} }
if !matchesAppliesTo(rule.AppliesTo, attrs) { if !matchesAppliesTo(rule.AppliesTo, attrs) {
continue continue
} }
if selected == nil || rule.Priority > highestPriority { if selected == nil || rule.Priority > highestPriority {
matched := rule copy := rule
selected = &matched selected = &copy
highestPriority = rule.Priority highestPriority = rule.Priority
continue continue
} }
if rule.Priority == highestPriority { if rule.Priority == highestPriority {
return nil, merrors.DataConflict("fees: conflicting fee rules") return nil, merrors.DataConflict("fees: conflicting fee rules")
} }
@@ -213,46 +167,25 @@ func selectRule(plan *model.FeePlan, trigger model.Trigger, asOf time.Time, attr
if selected == nil { if selected == nil {
return nil, merrors.NoData("fees: no applicable fee rule found") return nil, merrors.NoData("fees: no applicable fee rule found")
} }
return selected, nil return selected, nil
} }
func ruleIsActive(rule model.FeeRule, trigger model.Trigger, asOf time.Time) bool {
if rule.Trigger != trigger {
return false
}
if rule.EffectiveFrom.After(asOf) {
return false
}
if rule.EffectiveTo != nil && !rule.EffectiveTo.After(asOf) {
return false
}
return true
}
func matchesAppliesTo(appliesTo map[string]string, attrs map[string]string) bool { func matchesAppliesTo(appliesTo map[string]string, attrs map[string]string) bool {
if len(appliesTo) == 0 { if len(appliesTo) == 0 {
return true return true
} }
for key, value := range appliesTo { for key, value := range appliesTo {
if attrs == nil { if attrs == nil {
return false return false
} }
attrValue, ok := attrs[key] attrValue, ok := attrs[key]
if !ok { if !ok {
return false return false
} }
if !matchesAppliesValue(value, attrValue) { if !matchesAppliesValue(value, attrValue) {
return false return false
} }
} }
return true return true
} }
@@ -262,17 +195,16 @@ func matchesAppliesValue(expected, actual string) bool {
return actual == "" return actual == ""
} }
for value := range strings.SplitSeq(trimmed, ",") { values := strings.Split(trimmed, ",")
for _, value := range values {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
continue continue
} }
if value == "*" || value == actual { if value == "*" || value == actual {
return true return true
} }
} }
return false return false
} }
@@ -280,7 +212,6 @@ func zapFieldsForPlan(plan *model.FeePlan) []zap.Field {
if plan == nil { if plan == nil {
return []zap.Field{zap.Bool("plan_present", false)} return []zap.Field{zap.Bool("plan_present", false)}
} }
fields := []zap.Field{ fields := []zap.Field{
zap.Bool("plan_present", true), zap.Bool("plan_present", true),
zap.Bool("plan_active", plan.Active), zap.Bool("plan_active", plan.Active),
@@ -292,16 +223,13 @@ func zapFieldsForPlan(plan *model.FeePlan) []zap.Field {
} else { } else {
fields = append(fields, zap.Bool("plan_effective_to_set", false)) fields = append(fields, zap.Bool("plan_effective_to_set", false))
} }
if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() { if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
fields = append(fields, mzap.ObjRef("plan_org_ref", *plan.OrganizationRef)) fields = append(fields, mzap.ObjRef("plan_org_ref", *plan.OrganizationRef))
} else { } else {
fields = append(fields, zap.Bool("plan_org_ref_set", false)) fields = append(fields, zap.Bool("plan_org_ref_set", false))
} }
if plan.GetID() != nil && !plan.GetID().IsZero() { if plan.GetID() != nil && !plan.GetID().IsZero() {
fields = append(fields, mzap.StorableRef(plan)) fields = append(fields, mzap.StorableRef(plan))
} }
return fields return fields
} }

View File

@@ -13,7 +13,7 @@ import (
) )
func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) { func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
globalPlan := &model.FeePlan{ globalPlan := &model.FeePlan{
@@ -28,23 +28,20 @@ func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) {
resolver := New(store, zap.NewNop()) resolver := New(store, zap.NewNop())
orgA := bson.NewObjectID() orgA := bson.NewObjectID()
plan, rule, err := resolver.ResolveFeeRule(context.Background(), &orgA, model.TriggerCapture, now, nil) plan, rule, err := resolver.ResolveFeeRule(context.Background(), &orgA, model.TriggerCapture, now, nil)
if err != nil { if err != nil {
t.Fatalf("expected fallback to global, got error: %v", err) t.Fatalf("expected fallback to global, got error: %v", err)
} }
if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() { if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
t.Fatalf("expected global plan, got orgRef %s", plan.OrganizationRef.Hex()) t.Fatalf("expected global plan, got orgRef %s", plan.OrganizationRef.Hex())
} }
if rule.RuleID != "global_capture" { if rule.RuleID != "global_capture" {
t.Fatalf("unexpected rule selected: %s", rule.RuleID) t.Fatalf("unexpected rule selected: %s", rule.RuleID)
} }
} }
func TestResolver_OrgOverridesGlobal(t *testing.T) { func TestResolver_OrgOverridesGlobal(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
@@ -70,25 +67,22 @@ func TestResolver_OrgOverridesGlobal(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected org plan rule, got error: %v", err) t.Fatalf("expected org plan rule, got error: %v", err)
} }
if rule.RuleID != "org_capture" { if rule.RuleID != "org_capture" {
t.Fatalf("expected org rule, got %s", rule.RuleID) t.Fatalf("expected org rule, got %s", rule.RuleID)
} }
otherOrg := bson.NewObjectID() otherOrg := bson.NewObjectID()
_, rule, err = resolver.ResolveFeeRule(context.Background(), &otherOrg, model.TriggerCapture, now, nil) _, rule, err = resolver.ResolveFeeRule(context.Background(), &otherOrg, model.TriggerCapture, now, nil)
if err != nil { if err != nil {
t.Fatalf("expected global fallback for other org, got error: %v", err) t.Fatalf("expected global fallback for other org, got error: %v", err)
} }
if rule.RuleID != "global_capture" { if rule.RuleID != "global_capture" {
t.Fatalf("expected global rule, got %s", rule.RuleID) t.Fatalf("expected global rule, got %s", rule.RuleID)
} }
} }
func TestResolver_SelectsHighestPriority(t *testing.T) { func TestResolver_SelectsHighestPriority(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
@@ -109,7 +103,6 @@ func TestResolver_SelectsHighestPriority(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected rule resolution, got error: %v", err) t.Fatalf("expected rule resolution, got error: %v", err)
} }
if rule.RuleID != "high" { if rule.RuleID != "high" {
t.Fatalf("expected highest priority rule, got %s", rule.RuleID) t.Fatalf("expected highest priority rule, got %s", rule.RuleID)
} }
@@ -128,7 +121,7 @@ func TestResolver_SelectsHighestPriority(t *testing.T) {
} }
func TestResolver_EffectiveDateFiltering(t *testing.T) { func TestResolver_EffectiveDateFiltering(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
@@ -159,14 +152,13 @@ func TestResolver_EffectiveDateFiltering(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected fallback to global, got error: %v", err) t.Fatalf("expected fallback to global, got error: %v", err)
} }
if rule.RuleID != "current" { if rule.RuleID != "current" {
t.Fatalf("expected current global rule, got %s", rule.RuleID) t.Fatalf("expected current global rule, got %s", rule.RuleID)
} }
} }
func TestResolver_AppliesToFiltering(t *testing.T) { func TestResolver_AppliesToFiltering(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
plan := &model.FeePlan{ plan := &model.FeePlan{
@@ -184,7 +176,6 @@ func TestResolver_AppliesToFiltering(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected card rule, got error: %v", err) t.Fatalf("expected card rule, got error: %v", err)
} }
if rule.RuleID != "card" { if rule.RuleID != "card" {
t.Fatalf("expected card rule, got %s", rule.RuleID) t.Fatalf("expected card rule, got %s", rule.RuleID)
} }
@@ -193,14 +184,13 @@ func TestResolver_AppliesToFiltering(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected default rule, got error: %v", err) t.Fatalf("expected default rule, got error: %v", err)
} }
if rule.RuleID != "default" { if rule.RuleID != "default" {
t.Fatalf("expected default rule, got %s", rule.RuleID) t.Fatalf("expected default rule, got %s", rule.RuleID)
} }
} }
func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) { func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
plan := &model.FeePlan{ plan := &model.FeePlan{
@@ -219,7 +209,6 @@ func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected list match rule, got error: %v", err) t.Fatalf("expected list match rule, got error: %v", err)
} }
if rule.RuleID != "network_multi" { if rule.RuleID != "network_multi" {
t.Fatalf("expected network list rule, got %s", rule.RuleID) t.Fatalf("expected network list rule, got %s", rule.RuleID)
} }
@@ -228,7 +217,6 @@ func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected wildcard rule, got error: %v", err) t.Fatalf("expected wildcard rule, got error: %v", err)
} }
if rule.RuleID != "asset_any" { if rule.RuleID != "asset_any" {
t.Fatalf("expected asset wildcard rule, got %s", rule.RuleID) t.Fatalf("expected asset wildcard rule, got %s", rule.RuleID)
} }
@@ -237,14 +225,13 @@ func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected default rule, got error: %v", err) t.Fatalf("expected default rule, got error: %v", err)
} }
if rule.RuleID != "default" { if rule.RuleID != "default" {
t.Fatalf("expected default rule, got %s", rule.RuleID) t.Fatalf("expected default rule, got %s", rule.RuleID)
} }
} }
func TestResolver_MissingTriggerReturnsErr(t *testing.T) { func TestResolver_MissingTriggerReturnsErr(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
plan := &model.FeePlan{ plan := &model.FeePlan{
@@ -263,28 +250,28 @@ func TestResolver_MissingTriggerReturnsErr(t *testing.T) {
} }
func TestResolver_MultipleActivePlansConflict(t *testing.T) { func TestResolver_MultipleActivePlansConflict(t *testing.T) {
t.Parallel() t.Helper()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
plan1 := &model.FeePlan{ p1 := &model.FeePlan{
Active: true, Active: true,
EffectiveFrom: now.Add(-time.Hour), EffectiveFrom: now.Add(-time.Hour),
Rules: []model.FeeRule{ Rules: []model.FeeRule{
{RuleID: "r1", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)}, {RuleID: "r1", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
}, },
} }
plan1.OrganizationRef = &org p1.OrganizationRef = &org
plan2 := &model.FeePlan{ p2 := &model.FeePlan{
Active: true, Active: true,
EffectiveFrom: now.Add(-30 * time.Minute), EffectiveFrom: now.Add(-30 * time.Minute),
Rules: []model.FeeRule{ Rules: []model.FeeRule{
{RuleID: "r2", Trigger: model.TriggerCapture, Priority: 20, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)}, {RuleID: "r2", Trigger: model.TriggerCapture, Priority: 20, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
}, },
} }
plan2.OrganizationRef = &org p2.OrganizationRef = &org
store := &memoryPlansStore{plans: []*model.FeePlan{plan1, plan2}} store := &memoryPlansStore{plans: []*model.FeePlan{p1, p2}}
resolver := New(store, zap.NewNop()) resolver := New(store, zap.NewNop())
if _, _, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil); !errors.Is(err, storage.ErrConflictingFeePlans) { if _, _, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil); !errors.Is(err, storage.ErrConflictingFeePlans) {
@@ -302,83 +289,66 @@ func (m *memoryPlansStore) Get(context.Context, bson.ObjectID) (*model.FeePlan,
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
func (m *memoryPlansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) { func (m *memoryPlansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
if !orgRef.IsZero() { if !orgRef.IsZero() {
if plan, err := m.FindActiveOrgPlan(ctx, orgRef, asOf); err == nil { if plan, err := m.FindActiveOrgPlan(ctx, orgRef, at); err == nil {
return plan, nil return plan, nil
} else if !errors.Is(err, storage.ErrFeePlanNotFound) { } else if !errors.Is(err, storage.ErrFeePlanNotFound) {
return nil, err return nil, err
} }
} }
return m.FindActiveGlobalPlan(ctx, at)
return m.FindActiveGlobalPlan(ctx, asOf)
} }
func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) { func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
var matches []*model.FeePlan var matches []*model.FeePlan
for _, plan := range m.plans { for _, plan := range m.plans {
if plan == nil || plan.OrganizationRef == nil || plan.OrganizationRef.IsZero() || (*plan.OrganizationRef != orgRef) { if plan == nil || plan.OrganizationRef == nil || plan.OrganizationRef.IsZero() || (*plan.OrganizationRef != orgRef) {
continue continue
} }
if !plan.Active { if !plan.Active {
continue continue
} }
if plan.EffectiveFrom.After(at) {
if plan.EffectiveFrom.After(asOf) {
continue continue
} }
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(at) {
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(asOf) {
continue continue
} }
matches = append(matches, plan) matches = append(matches, plan)
} }
if len(matches) == 0 { if len(matches) == 0 {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if len(matches) > 1 { if len(matches) > 1 {
return nil, storage.ErrConflictingFeePlans return nil, storage.ErrConflictingFeePlans
} }
return matches[0], nil return matches[0], nil
} }
func (m *memoryPlansStore) FindActiveGlobalPlan(_ context.Context, asOf time.Time) (*model.FeePlan, error) { func (m *memoryPlansStore) FindActiveGlobalPlan(_ context.Context, at time.Time) (*model.FeePlan, error) {
var matches []*model.FeePlan var matches []*model.FeePlan
for _, plan := range m.plans { for _, plan := range m.plans {
if plan == nil || ((plan.OrganizationRef != nil) && !plan.OrganizationRef.IsZero()) { if plan == nil || ((plan.OrganizationRef != nil) && !plan.OrganizationRef.IsZero()) {
continue continue
} }
if !plan.Active { if !plan.Active {
continue continue
} }
if plan.EffectiveFrom.After(at) {
if plan.EffectiveFrom.After(asOf) {
continue continue
} }
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(at) {
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(asOf) {
continue continue
} }
matches = append(matches, plan) matches = append(matches, plan)
} }
if len(matches) == 0 { if len(matches) == 0 {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if len(matches) > 1 { if len(matches) > 1 {
return nil, storage.ErrConflictingFeePlans return nil, storage.ErrConflictingFeePlans
} }
return matches[0], nil return matches[0], nil
} }

View File

@@ -12,7 +12,6 @@ import (
func requestLogFields(meta *feesv1.RequestMeta, intent *feesv1.Intent) []zap.Field { func requestLogFields(meta *feesv1.RequestMeta, intent *feesv1.Intent) []zap.Field {
fields := logFieldsFromRequestMeta(meta) fields := logFieldsFromRequestMeta(meta)
fields = append(fields, logFieldsFromIntent(intent)...) fields = append(fields, logFieldsFromIntent(intent)...)
return fields return fields
} }
@@ -20,14 +19,11 @@ func logFieldsFromRequestMeta(meta *feesv1.RequestMeta) []zap.Field {
if meta == nil { if meta == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 4) fields := make([]zap.Field, 0, 4)
if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" { if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" {
fields = append(fields, zap.String("organization_ref", org)) fields = append(fields, zap.String("organization_ref", org))
} }
fields = append(fields, logFieldsFromTrace(meta.GetTrace())...) fields = append(fields, logFieldsFromTrace(meta.GetTrace())...)
return fields return fields
} }
@@ -35,30 +31,24 @@ func logFieldsFromIntent(intent *feesv1.Intent) []zap.Field {
if intent == nil { if intent == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 5) fields := make([]zap.Field, 0, 5)
if trigger := intent.GetTrigger(); trigger != feesv1.Trigger_TRIGGER_UNSPECIFIED { if trigger := intent.GetTrigger(); trigger != feesv1.Trigger_TRIGGER_UNSPECIFIED {
fields = append(fields, zap.String("trigger", trigger.String())) fields = append(fields, zap.String("trigger", trigger.String()))
} }
if base := intent.GetBaseAmount(); base != nil { if base := intent.GetBaseAmount(); base != nil {
if amount := strings.TrimSpace(base.GetAmount()); amount != "" { if amount := strings.TrimSpace(base.GetAmount()); amount != "" {
fields = append(fields, zap.String("base_amount", amount)) fields = append(fields, zap.String("base_amount", amount))
} }
if currency := strings.TrimSpace(base.GetCurrency()); currency != "" { if currency := strings.TrimSpace(base.GetCurrency()); currency != "" {
fields = append(fields, zap.String("base_currency", currency)) fields = append(fields, zap.String("base_currency", currency))
} }
} }
if booked := intent.GetBookedAt(); booked != nil && booked.IsValid() { if booked := intent.GetBookedAt(); booked != nil && booked.IsValid() {
fields = append(fields, zap.Time("booked_at", booked.AsTime())) fields = append(fields, zap.Time("booked_at", booked.AsTime()))
} }
if attrs := intent.GetAttributes(); len(attrs) > 0 { if attrs := intent.GetAttributes(); len(attrs) > 0 {
fields = append(fields, zap.Int("attributes_count", len(attrs))) fields = append(fields, zap.Int("attributes_count", len(attrs)))
} }
return fields return fields
} }
@@ -66,20 +56,16 @@ func logFieldsFromTrace(trace *tracev1.TraceContext) []zap.Field {
if trace == nil { if trace == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 3) fields := make([]zap.Field, 0, 3)
if reqRef := strings.TrimSpace(trace.GetRequestRef()); reqRef != "" { if reqRef := strings.TrimSpace(trace.GetRequestRef()); reqRef != "" {
fields = append(fields, zap.String("request_ref", reqRef)) fields = append(fields, zap.String("request_ref", reqRef))
} }
if idem := strings.TrimSpace(trace.GetIdempotencyKey()); idem != "" { if idem := strings.TrimSpace(trace.GetIdempotencyKey()); idem != "" {
fields = append(fields, zap.String("idempotency_key", idem)) fields = append(fields, zap.String("idempotency_key", idem))
} }
if traceRef := strings.TrimSpace(trace.GetTraceRef()); traceRef != "" { if traceRef := strings.TrimSpace(trace.GetTraceRef()); traceRef != "" {
fields = append(fields, zap.String("trace_ref", traceRef)) fields = append(fields, zap.String("trace_ref", traceRef))
} }
return fields return fields
} }
@@ -87,20 +73,16 @@ func logFieldsFromTokenPayload(payload *feeQuoteTokenPayload) []zap.Field {
if payload == nil { if payload == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 6) fields := make([]zap.Field, 0, 6)
if org := strings.TrimSpace(payload.OrganizationRef); org != "" { if org := strings.TrimSpace(payload.OrganizationRef); org != "" {
fields = append(fields, zap.String("organization_ref", org)) fields = append(fields, zap.String("organization_ref", org))
} }
if payload.ExpiresAtUnixMs > 0 { if payload.ExpiresAtUnixMs > 0 {
fields = append(fields, fields = append(fields,
zap.Int64("expires_at_unix_ms", payload.ExpiresAtUnixMs), zap.Int64("expires_at_unix_ms", payload.ExpiresAtUnixMs),
zap.Time("expires_at", time.UnixMilli(payload.ExpiresAtUnixMs))) zap.Time("expires_at", time.UnixMilli(payload.ExpiresAtUnixMs)))
} }
fields = append(fields, logFieldsFromIntent(payload.Intent)...) fields = append(fields, logFieldsFromIntent(payload.Intent)...)
fields = append(fields, logFieldsFromTrace(payload.Trace)...) fields = append(fields, logFieldsFromTrace(payload.Trace)...)
return fields return fields
} }

View File

@@ -50,7 +50,6 @@ func observeMetrics(call string, trigger feesv1.Trigger, statusLabel string, fxU
if trigger == feesv1.Trigger_TRIGGER_UNSPECIFIED { if trigger == feesv1.Trigger_TRIGGER_UNSPECIFIED {
triggerLabel = "TRIGGER_UNSPECIFIED" triggerLabel = "TRIGGER_UNSPECIFIED"
} }
fxLabel := strconv.FormatBool(fxUsed) fxLabel := strconv.FormatBool(fxUsed)
quoteRequestsTotal.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Inc() quoteRequestsTotal.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Inc()
quoteLatency.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Observe(took.Seconds()) quoteLatency.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Observe(took.Seconds())
@@ -60,16 +59,13 @@ func statusFromError(err error) string {
if err == nil { if err == nil {
return "success" return "success"
} }
st, ok := status.FromError(err) st, ok := status.FromError(err)
if !ok { if !ok {
return "error" return "error"
} }
code := st.Code() code := st.Code()
if code == codes.OK { if code == codes.OK {
return "success" return "success"
} }
return strings.ToLower(code.String()) return strings.ToLower(code.String())
} }

View File

@@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"strings" "strings"
"time" "time"
@@ -32,8 +33,6 @@ import (
) )
type Service struct { type Service struct {
feesv1.UnimplementedFeeEngineServer
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
producer msg.Producer producer msg.Producer
@@ -43,6 +42,7 @@ type Service struct {
resolver FeeResolver resolver FeeResolver
announcer *discovery.Announcer announcer *discovery.Announcer
invokeURI string invokeURI string
feesv1.UnimplementedFeeEngineServer
} }
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service { func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
@@ -52,7 +52,6 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
producer: producer, producer: producer,
clock: clockpkg.NewSystem(), clock: clockpkg.NewSystem(),
} }
initMetrics() initMetrics()
for _, opt := range opts { for _, opt := range opts {
@@ -62,11 +61,9 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
if svc.clock == nil { if svc.clock == nil {
svc.clock = clockpkg.NewSystem() svc.clock = clockpkg.NewSystem()
} }
if svc.calculator == nil { if svc.calculator == nil {
svc.calculator = internalcalculator.New(svc.logger, svc.oracle) svc.calculator = internalcalculator.New(svc.logger, svc.oracle)
} }
if svc.resolver == nil { if svc.resolver == nil {
svc.resolver = resolver.New(repo.Plans(), svc.logger) svc.resolver = resolver.New(repo.Plans(), svc.logger)
} }
@@ -86,12 +83,25 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
if s.announcer != nil { if s.announcer != nil {
s.announcer.Stop() s.announcer.Stop()
} }
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: "BILLING_FEES",
Operations: []string{"fee.calc"},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.FeePlans), announce)
s.announcer.Start()
}
func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (resp *feesv1.QuoteFeesResponse, err error) { func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (resp *feesv1.QuoteFeesResponse, err error) {
var ( var (
meta *feesv1.RequestMeta meta *feesv1.RequestMeta
@@ -101,29 +111,23 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
meta = req.GetMeta() meta = req.GetMeta()
intent = req.GetIntent() intent = req.GetIntent()
} }
logger := s.logger.With(requestLogFields(meta, intent)...) logger := s.logger.With(requestLogFields(meta, intent)...)
start := s.clock.Now() start := s.clock.Now()
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
if intent != nil { if intent != nil {
trigger = intent.GetTrigger() trigger = intent.GetTrigger()
} }
var fxUsed bool var fxUsed bool
defer func() { defer func() {
statusLabel := statusFromError(err) statusLabel := statusFromError(err)
linesCount := 0 linesCount := 0
appliedCount := 0 appliedCount := 0
if err == nil && resp != nil { if err == nil && resp != nil {
fxUsed = resp.GetFxUsed() != nil fxUsed = resp.GetFxUsed() != nil
linesCount = len(resp.GetLines()) linesCount = len(resp.GetLines())
appliedCount = len(resp.GetApplied()) appliedCount = len(resp.GetApplied())
} }
observeMetrics("quote", trigger, statusLabel, fxUsed, time.Since(start)) observeMetrics("quote", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{ logFields := []zap.Field{
@@ -136,10 +140,8 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
} }
if err != nil { if err != nil {
logger.Warn("QuoteFees finished", append(logFields, zap.Error(err))...) logger.Warn("QuoteFees finished", append(logFields, zap.Error(err))...)
return return
} }
logger.Info("QuoteFees finished", logFields...) logger.Info("QuoteFees finished", logFields...)
}() }()
@@ -153,14 +155,12 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
if parseErr != nil { if parseErr != nil {
logger.Warn("QuoteFees invalid organization_ref", zap.Error(parseErr)) logger.Warn("QuoteFees invalid organization_ref", zap.Error(parseErr))
err = status.Error(codes.InvalidArgument, "invalid organization_ref") err = status.Error(codes.InvalidArgument, "invalid organization_ref")
return nil, err return nil, err
} }
lines, applied, fxResult, computeErr := s.computeQuote(ctx, orgRef, req.GetIntent(), req.GetPolicy(), req.GetMeta().GetTrace()) lines, applied, fx, computeErr := s.computeQuote(ctx, orgRef, req.GetIntent(), req.GetPolicy(), req.GetMeta().GetTrace())
if computeErr != nil { if computeErr != nil {
err = computeErr err = computeErr
return nil, err return nil, err
} }
@@ -168,9 +168,8 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
Meta: &feesv1.ResponseMeta{Trace: req.GetMeta().GetTrace()}, Meta: &feesv1.ResponseMeta{Trace: req.GetMeta().GetTrace()},
Lines: lines, Lines: lines,
Applied: applied, Applied: applied,
FxUsed: fxResult, FxUsed: fx,
} }
return resp, nil return resp, nil
} }
@@ -183,17 +182,48 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
meta = req.GetMeta() meta = req.GetMeta()
intent = req.GetIntent() intent = req.GetIntent()
} }
logger := s.logger.With(requestLogFields(meta, intent)...) logger := s.logger.With(requestLogFields(meta, intent)...)
start := s.clock.Now() start := s.clock.Now()
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
if intent != nil { if intent != nil {
trigger = intent.GetTrigger() trigger = intent.GetTrigger()
} }
var (
fxUsed bool
expiresAt time.Time
)
defer func() {
statusLabel := statusFromError(err)
linesCount := 0
appliedCount := 0
if err == nil && resp != nil {
fxUsed = resp.GetFxUsed() != nil
linesCount = len(resp.GetLines())
appliedCount = len(resp.GetApplied())
if ts := resp.GetExpiresAt(); ts != nil {
expiresAt = ts.AsTime()
}
}
observeMetrics("precompute", trigger, statusLabel, fxUsed, time.Since(start))
defer func() { s.observePrecomputeFees(logger, err, resp, trigger, start) }() logFields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()),
zap.Int("lines", linesCount),
zap.Int("applied_rules", appliedCount),
}
if !expiresAt.IsZero() {
logFields = append(logFields, zap.Time("expires_at", expiresAt))
}
if err != nil {
logger.Warn("PrecomputeFees finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("PrecomputeFees finished", logFields...)
}()
logger.Debug("PrecomputeFees request received") logger.Debug("PrecomputeFees request received")
@@ -207,14 +237,12 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
if parseErr != nil { if parseErr != nil {
logger.Warn("PrecomputeFees invalid organization_ref", zap.Error(parseErr)) logger.Warn("PrecomputeFees invalid organization_ref", zap.Error(parseErr))
err = status.Error(codes.InvalidArgument, "invalid organization_ref") err = status.Error(codes.InvalidArgument, "invalid organization_ref")
return nil, err return nil, err
} }
lines, applied, fxResult, computeErr := s.computeQuoteWithTime(ctx, orgRef, req.GetIntent(), nil, req.GetMeta().GetTrace(), now) lines, applied, fx, computeErr := s.computeQuoteWithTime(ctx, orgRef, req.GetIntent(), nil, req.GetMeta().GetTrace(), now)
if computeErr != nil { if computeErr != nil {
err = computeErr err = computeErr
return nil, err return nil, err
} }
@@ -222,8 +250,7 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
if ttl <= 0 { if ttl <= 0 {
ttl = 60000 ttl = 60000
} }
expiresAt = now.Add(time.Duration(ttl) * time.Millisecond)
expiresAt := now.Add(time.Duration(ttl) * time.Millisecond)
payload := feeQuoteTokenPayload{ payload := feeQuoteTokenPayload{
OrganizationRef: req.GetMeta().GetOrganizationRef(), OrganizationRef: req.GetMeta().GetOrganizationRef(),
@@ -234,9 +261,8 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
var token string var token string
if token, err = encodeTokenPayload(payload); err != nil { if token, err = encodeTokenPayload(payload); err != nil {
logger.Warn("Failed to encode fee quote token", zap.Error(err)) logger.Warn("failed to encode fee quote token", zap.Error(err))
err = status.Error(codes.Internal, "failed to encode fee quote token") err = status.Error(codes.Internal, "failed to encode fee quote token")
return nil, err return nil, err
} }
@@ -246,9 +272,8 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
ExpiresAt: timestamppb.New(expiresAt), ExpiresAt: timestamppb.New(expiresAt),
Lines: lines, Lines: lines,
Applied: applied, Applied: applied,
FxUsed: fxResult, FxUsed: fx,
} }
return resp, nil return resp, nil
} }
@@ -257,23 +282,49 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
if req != nil { if req != nil {
tokenLen = len(strings.TrimSpace(req.GetFeeQuoteToken())) tokenLen = len(strings.TrimSpace(req.GetFeeQuoteToken()))
} }
logger := s.logger.With(zap.Int("token_length", tokenLen)) logger := s.logger.With(zap.Int("token_length", tokenLen))
start := s.clock.Now() start := s.clock.Now()
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
var (
fxUsed bool
resultReason string
)
defer func() {
statusLabel := statusFromError(err)
if err == nil && resp != nil {
if !resp.GetValid() {
statusLabel = "invalid"
}
fxUsed = resp.GetFxUsed() != nil
if resp.GetIntent() != nil {
trigger = resp.GetIntent().GetTrigger()
}
}
observeMetrics("validate", trigger, statusLabel, fxUsed, time.Since(start))
var resultReason string logFields := []zap.Field{
zap.String("status", statusLabel),
defer func() { s.observeValidateFeeToken(logger, err, resp, trigger, resultReason, start) }() zap.Duration("duration", time.Since(start)),
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()),
zap.Bool("valid", resp != nil && resp.GetValid()),
}
if resultReason != "" {
logFields = append(logFields, zap.String("reason", resultReason))
}
if err != nil {
logger.Warn("ValidateFeeToken finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("ValidateFeeToken finished", logFields...)
}()
logger.Debug("ValidateFeeToken request received") logger.Debug("ValidateFeeToken request received")
if req == nil || strings.TrimSpace(req.GetFeeQuoteToken()) == "" { if req == nil || strings.TrimSpace(req.GetFeeQuoteToken()) == "" {
resultReason = "missing_token" resultReason = "missing_token"
err = status.Error(codes.InvalidArgument, "fee_quote_token is required") err = status.Error(codes.InvalidArgument, "fee_quote_token is required")
return nil, err return nil, err
} }
@@ -282,11 +333,8 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken()) payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken())
if decodeErr != nil { if decodeErr != nil {
resultReason = "invalid_token" resultReason = "invalid_token"
logger.Warn("failed to decode fee quote token", zap.Error(decodeErr))
logger.Warn("Failed to decode fee quote token", zap.Error(decodeErr))
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
return resp, nil return resp, nil
} }
@@ -298,29 +346,22 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
if now.UnixMilli() > payload.ExpiresAtUnixMs { if now.UnixMilli() > payload.ExpiresAtUnixMs {
resultReason = "expired" resultReason = "expired"
logger.Info("fee quote token expired")
logger.Info("Fee quote token expired")
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"}
return resp, nil return resp, nil
} }
orgRef, parseErr := bson.ObjectIDFromHex(payload.OrganizationRef) orgRef, parseErr := bson.ObjectIDFromHex(payload.OrganizationRef)
if parseErr != nil { if parseErr != nil {
resultReason = "invalid_token" resultReason = "invalid_token"
logger.Warn("token contained invalid organization reference", zap.Error(parseErr))
logger.Warn("Token contained invalid organization reference", zap.Error(parseErr))
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
return resp, nil return resp, nil
} }
lines, applied, fxResult, computeErr := s.computeQuoteWithTime(ctx, orgRef, payload.Intent, nil, payload.Trace, now) lines, applied, fx, computeErr := s.computeQuoteWithTime(ctx, orgRef, payload.Intent, nil, payload.Trace, now)
if computeErr != nil { if computeErr != nil {
err = computeErr err = computeErr
return nil, err return nil, err
} }
@@ -330,9 +371,8 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
Intent: payload.Intent, Intent: payload.Intent,
Lines: lines, Lines: lines,
Applied: applied, Applied: applied,
FxUsed: fxResult, FxUsed: fx,
} }
return resp, nil return resp, nil
} }
@@ -340,31 +380,24 @@ func (s *Service) validateQuoteRequest(req *feesv1.QuoteFeesRequest) error {
if req == nil { if req == nil {
return status.Error(codes.InvalidArgument, "request is required") return status.Error(codes.InvalidArgument, "request is required")
} }
if req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetOrganizationRef()) == "" { if req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetOrganizationRef()) == "" {
return status.Error(codes.InvalidArgument, "meta.organization_ref is required") return status.Error(codes.InvalidArgument, "meta.organization_ref is required")
} }
if req.GetIntent() == nil { if req.GetIntent() == nil {
return status.Error(codes.InvalidArgument, "intent is required") return status.Error(codes.InvalidArgument, "intent is required")
} }
if req.GetIntent().GetTrigger() == feesv1.Trigger_TRIGGER_UNSPECIFIED { if req.GetIntent().GetTrigger() == feesv1.Trigger_TRIGGER_UNSPECIFIED {
return status.Error(codes.InvalidArgument, "intent.trigger is required") return status.Error(codes.InvalidArgument, "intent.trigger is required")
} }
if req.GetIntent().GetBaseAmount() == nil { if req.GetIntent().GetBaseAmount() == nil {
return status.Error(codes.InvalidArgument, "intent.base_amount is required") return status.Error(codes.InvalidArgument, "intent.base_amount is required")
} }
if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetAmount()) == "" { if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetAmount()) == "" {
return status.Error(codes.InvalidArgument, "intent.base_amount.amount is required") return status.Error(codes.InvalidArgument, "intent.base_amount.amount is required")
} }
if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetCurrency()) == "" { if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetCurrency()) == "" {
return status.Error(codes.InvalidArgument, "intent.base_amount.currency is required") return status.Error(codes.InvalidArgument, "intent.base_amount.currency is required")
} }
return nil return nil
} }
@@ -372,7 +405,6 @@ func (s *Service) validatePrecomputeRequest(req *feesv1.PrecomputeFeesRequest) e
if req == nil { if req == nil {
return status.Error(codes.InvalidArgument, "request is required") return status.Error(codes.InvalidArgument, "request is required")
} }
return s.validateQuoteRequest(&feesv1.QuoteFeesRequest{Meta: req.GetMeta(), Intent: req.GetIntent()}) return s.validateQuoteRequest(&feesv1.QuoteFeesRequest{Meta: req.GetMeta(), Intent: req.GetIntent()})
} }
@@ -381,13 +413,17 @@ func (s *Service) computeQuote(ctx context.Context, orgRef bson.ObjectID, intent
} }
func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID, intent *feesv1.Intent, _ *feesv1.PolicyOverrides, trace *tracev1.TraceContext, now time.Time) ([]*feesv1.DerivedPostingLine, []*feesv1.AppliedRule, *feesv1.FXUsed, error) { func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID, intent *feesv1.Intent, _ *feesv1.PolicyOverrides, trace *tracev1.TraceContext, now time.Time) ([]*feesv1.DerivedPostingLine, []*feesv1.AppliedRule, *feesv1.FXUsed, error) {
bookedAt := resolvedBookedAt(intent, now) bookedAt := now
if intent.GetBookedAt() != nil && intent.GetBookedAt().IsValid() {
bookedAt = intent.GetBookedAt().AsTime()
}
logFields := []zap.Field{zap.Time("booked_at_used", bookedAt)} logFields := []zap.Field{
zap.Time("booked_at_used", bookedAt),
}
if !orgRef.IsZero() { if !orgRef.IsZero() {
logFields = append(logFields, zap.String("organization_ref", orgRef.Hex())) logFields = append(logFields, zap.String("organization_ref", orgRef.Hex()))
} }
logFields = append(logFields, logFieldsFromIntent(intent)...) logFields = append(logFields, logFieldsFromIntent(intent)...)
logFields = append(logFields, logFieldsFromTrace(trace)...) logFields = append(logFields, logFieldsFromTrace(trace)...)
logger := s.logger.With(logFields...) logger := s.logger.With(logFields...)
@@ -400,13 +436,22 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
plan, rule, err := s.resolver.ResolveFeeRule(ctx, orgPtr, convertTrigger(intent.GetTrigger()), bookedAt, intent.GetAttributes()) plan, rule, err := s.resolver.ResolveFeeRule(ctx, orgPtr, convertTrigger(intent.GetTrigger()), bookedAt, intent.GetAttributes())
if err != nil { if err != nil {
s.logger.Warn("Failed to resolve fee rule", zap.Error(err)) s.logger.Warn("Failed to resolve fee rule", zap.Error(err))
switch {
return nil, nil, nil, mapResolveError(err) case errors.Is(err, merrors.ErrNoData):
return nil, nil, nil, status.Error(codes.NotFound, fmt.Sprintf("fee rule not found: %s", err.Error()))
case errors.Is(err, merrors.ErrDataConflict):
return nil, nil, nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("conflicting fee rules: %s", err.Error()))
case errors.Is(err, storage.ErrConflictingFeePlans):
return nil, nil, nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("conflicting fee plans: %s", err.Error()))
case errors.Is(err, storage.ErrFeePlanNotFound):
return nil, nil, nil, status.Error(codes.NotFound, fmt.Sprintf("fee plan not found: %s", err.Error()))
default:
return nil, nil, nil, status.Error(codes.Internal, fmt.Sprintf("failed to resolve fee rule: %s", err.Error()))
}
} }
originalRules := plan.Rules originalRules := plan.Rules
plan.Rules = []model.FeeRule{*rule} plan.Rules = []model.FeeRule{*rule}
defer func() { defer func() {
plan.Rules = originalRules plan.Rules = originalRules
}() }()
@@ -416,116 +461,13 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
if errors.Is(calcErr, merrors.ErrInvalidArg) { if errors.Is(calcErr, merrors.ErrInvalidArg) {
return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error()) return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error())
} }
logger.Warn("failed to compute fee quote", zap.Error(calcErr))
logger.Warn("Failed to compute fee quote", zap.Error(calcErr))
return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote") return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote")
} }
return result.Lines, result.Applied, result.FxUsed, nil return result.Lines, result.Applied, result.FxUsed, nil
} }
func resolvedBookedAt(intent *feesv1.Intent, now time.Time) time.Time {
if intent.GetBookedAt() != nil && intent.GetBookedAt().IsValid() {
return intent.GetBookedAt().AsTime()
}
return now
}
func mapResolveError(err error) error {
switch {
case errors.Is(err, merrors.ErrNoData):
return status.Error(codes.NotFound, "fee rule not found: "+err.Error())
case errors.Is(err, merrors.ErrDataConflict):
return status.Error(codes.FailedPrecondition, "conflicting fee rules: "+err.Error())
case errors.Is(err, storage.ErrConflictingFeePlans):
return status.Error(codes.FailedPrecondition, "conflicting fee plans: "+err.Error())
case errors.Is(err, storage.ErrFeePlanNotFound):
return status.Error(codes.NotFound, "fee plan not found: "+err.Error())
default:
return status.Error(codes.Internal, "failed to resolve fee rule: "+err.Error())
}
}
func (s *Service) observePrecomputeFees(logger mlogger.Logger, err error, resp *feesv1.PrecomputeFeesResponse, trigger feesv1.Trigger, start time.Time) {
statusLabel := statusFromError(err)
fxUsed := false
linesCount := 0
appliedCount := 0
var expiresAt time.Time
if err == nil && resp != nil {
fxUsed = resp.GetFxUsed() != nil
linesCount = len(resp.GetLines())
appliedCount = len(resp.GetApplied())
if ts := resp.GetExpiresAt(); ts != nil {
expiresAt = ts.AsTime()
}
}
observeMetrics("precompute", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()),
zap.Int("lines", linesCount),
zap.Int("applied_rules", appliedCount),
}
if !expiresAt.IsZero() {
logFields = append(logFields, zap.Time("expires_at", expiresAt))
}
if err != nil {
logger.Warn("PrecomputeFees finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("PrecomputeFees finished", logFields...)
}
func (s *Service) observeValidateFeeToken(logger mlogger.Logger, err error, resp *feesv1.ValidateFeeTokenResponse, trigger feesv1.Trigger, resultReason string, start time.Time) {
statusLabel := statusFromError(err)
fxUsed := false
if err == nil && resp != nil {
if !resp.GetValid() {
statusLabel = "invalid"
}
fxUsed = resp.GetFxUsed() != nil
if resp.GetIntent() != nil {
trigger = resp.GetIntent().GetTrigger()
}
}
observeMetrics("validate", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()),
zap.Bool("valid", resp != nil && resp.GetValid()),
}
if resultReason != "" {
logFields = append(logFields, zap.String("reason", resultReason))
}
if err != nil {
logger.Warn("ValidateFeeToken finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("ValidateFeeToken finished", logFields...)
}
type feeQuoteTokenPayload struct { type feeQuoteTokenPayload struct {
OrganizationRef string `json:"organization_ref"` OrganizationRef string `json:"organization_ref"`
Intent *feesv1.Intent `json:"intent"` Intent *feesv1.Intent `json:"intent"`
@@ -538,36 +480,17 @@ func encodeTokenPayload(payload feeQuoteTokenPayload) (string, error) {
if err != nil { if err != nil {
return "", merrors.Internal("fees: failed to serialize token payload") return "", merrors.Internal("fees: failed to serialize token payload")
} }
return base64.StdEncoding.EncodeToString(data), nil return base64.StdEncoding.EncodeToString(data), nil
} }
func decodeTokenPayload(token string) (feeQuoteTokenPayload, error) { func decodeTokenPayload(token string) (feeQuoteTokenPayload, error) {
var payload feeQuoteTokenPayload var payload feeQuoteTokenPayload
data, err := base64.StdEncoding.DecodeString(token) data, err := base64.StdEncoding.DecodeString(token)
if err != nil { if err != nil {
return payload, merrors.InvalidArgument("fees: invalid token encoding") return payload, merrors.InvalidArgument("fees: invalid token encoding")
} }
if err := json.Unmarshal(data, &payload); err != nil { if err := json.Unmarshal(data, &payload); err != nil {
return payload, merrors.InvalidArgument("fees: invalid token payload") return payload, merrors.InvalidArgument("fees: invalid token payload")
} }
return payload, nil return payload, nil
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: "BILLING_FEES",
Operations: []string{discovery.OperationFeeCalc},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.FeePlans, announce)
s.announcer.Start()
}

View File

@@ -20,7 +20,7 @@ import (
) )
func TestQuoteFees_ComputesDerivedLines(t *testing.T) { func TestQuoteFees_ComputesDerivedLines(t *testing.T) {
t.Parallel() t.Helper()
now := time.Date(2024, 1, 10, 16, 0, 0, 0, time.UTC) now := time.Date(2024, 1, 10, 16, 0, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -93,11 +93,9 @@ func TestQuoteFees_ComputesDerivedLines(t *testing.T) {
if got := line.GetMoney().GetAmount(); got != "3.20" { if got := line.GetMoney().GetAmount(); got != "3.20" {
t.Fatalf("expected fee amount 3.20, got %s", got) t.Fatalf("expected fee amount 3.20, got %s", got)
} }
if line.GetMoney().GetCurrency() != "USD" { if line.GetMoney().GetCurrency() != "USD" {
t.Fatalf("expected currency USD, got %s", line.GetMoney().GetCurrency()) t.Fatalf("expected currency USD, got %s", line.GetMoney().GetCurrency())
} }
if line.GetLedgerAccountRef() != "acct:fees" { if line.GetLedgerAccountRef() != "acct:fees" {
t.Fatalf("unexpected ledger account ref %s", line.GetLedgerAccountRef()) t.Fatalf("unexpected ledger account ref %s", line.GetLedgerAccountRef())
} }
@@ -113,18 +111,16 @@ func TestQuoteFees_ComputesDerivedLines(t *testing.T) {
if applied.GetTaxCode() != "VAT" || applied.GetTaxRate() != "0.20" { if applied.GetTaxCode() != "VAT" || applied.GetTaxRate() != "0.20" {
t.Fatalf("applied rule metadata mismatch: %+v", applied) t.Fatalf("applied rule metadata mismatch: %+v", applied)
} }
if applied.GetRounding() != moneyv1.RoundingMode_ROUND_HALF_UP { if applied.GetRounding() != moneyv1.RoundingMode_ROUND_HALF_UP {
t.Fatalf("expected rounding HALF_UP, got %v", applied.GetRounding()) t.Fatalf("expected rounding HALF_UP, got %v", applied.GetRounding())
} }
if applied.GetParameters()["scale"] != "2" { if applied.GetParameters()["scale"] != "2" {
t.Fatalf("expected parameters to carry metadata scale, got %+v", applied.GetParameters()) t.Fatalf("expected parameters to carry metadata scale, got %+v", applied.GetParameters())
} }
} }
func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) { func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) {
t.Parallel() t.Helper()
now := time.Date(2024, 5, 20, 9, 30, 0, 0, time.UTC) now := time.Date(2024, 5, 20, 9, 30, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -193,23 +189,20 @@ func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("QuoteFees returned error: %v", err) t.Fatalf("QuoteFees returned error: %v", err)
} }
if len(resp.GetLines()) != 1 { if len(resp.GetLines()) != 1 {
t.Fatalf("expected only base rule to fire, got %d lines", len(resp.GetLines())) t.Fatalf("expected only base rule to fire, got %d lines", len(resp.GetLines()))
} }
line := resp.GetLines()[0] line := resp.GetLines()[0]
if line.GetLedgerAccountRef() != "acct:base" { if line.GetLedgerAccountRef() != "acct:base" {
t.Fatalf("expected base rule to apply, got %s", line.GetLedgerAccountRef()) t.Fatalf("expected base rule to apply, got %s", line.GetLedgerAccountRef())
} }
if line.GetMoney().GetAmount() != "5.00" { if line.GetMoney().GetAmount() != "5.00" {
t.Fatalf("expected 5.00 amount, got %s", line.GetMoney().GetAmount()) t.Fatalf("expected 5.00 amount, got %s", line.GetMoney().GetAmount())
} }
} }
func TestQuoteFees_RoundingDown(t *testing.T) { func TestQuoteFees_RoundingDown(t *testing.T) {
t.Parallel() t.Helper()
now := time.Date(2024, 3, 15, 12, 0, 0, 0, time.UTC) now := time.Date(2024, 3, 15, 12, 0, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -256,18 +249,16 @@ func TestQuoteFees_RoundingDown(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("QuoteFees returned error: %v", err) t.Fatalf("QuoteFees returned error: %v", err)
} }
if len(resp.GetLines()) != 1 { if len(resp.GetLines()) != 1 {
t.Fatalf("expected single derived line, got %d", len(resp.GetLines())) t.Fatalf("expected single derived line, got %d", len(resp.GetLines()))
} }
if resp.GetLines()[0].GetMoney().GetAmount() != "0.01" { if resp.GetLines()[0].GetMoney().GetAmount() != "0.01" {
t.Fatalf("expected rounding down to 0.01, got %s", resp.GetLines()[0].GetMoney().GetAmount()) t.Fatalf("expected rounding down to 0.01, got %s", resp.GetLines()[0].GetMoney().GetAmount())
} }
} }
func TestQuoteFees_UsesInjectedCalculator(t *testing.T) { func TestQuoteFees_UsesInjectedCalculator(t *testing.T) {
t.Parallel() t.Helper()
now := time.Date(2024, 6, 1, 8, 0, 0, 0, time.UTC) now := time.Date(2024, 6, 1, 8, 0, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -325,26 +316,22 @@ func TestQuoteFees_UsesInjectedCalculator(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("QuoteFees returned error: %v", err) t.Fatalf("QuoteFees returned error: %v", err)
} }
if !calc.called { if !calc.called {
t.Fatalf("expected calculator to be invoked") t.Fatalf("expected calculator to be invoked")
} }
if calc.gotPlan != plan { if calc.gotPlan != plan {
t.Fatalf("expected calculator to receive plan pointer") t.Fatalf("expected calculator to receive plan pointer")
} }
if len(resp.GetLines()) != len(result.Lines) { if len(resp.GetLines()) != len(result.Lines) {
t.Fatalf("expected %d lines, got %d", len(result.Lines), len(resp.GetLines())) t.Fatalf("expected %d lines, got %d", len(result.Lines), len(resp.GetLines()))
} }
if resp.GetLines()[0].GetLedgerAccountRef() != "acct:stub" { if resp.GetLines()[0].GetLedgerAccountRef() != "acct:stub" {
t.Fatalf("unexpected ledger account in response: %s", resp.GetLines()[0].GetLedgerAccountRef()) t.Fatalf("unexpected ledger account in response: %s", resp.GetLines()[0].GetLedgerAccountRef())
} }
} }
func TestQuoteFees_PopulatesFxUsed(t *testing.T) { func TestQuoteFees_PopulatesFxUsed(t *testing.T) {
t.Parallel() t.Helper()
now := time.Date(2024, 7, 1, 9, 30, 0, 0, time.UTC) now := time.Date(2024, 7, 1, 9, 30, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -369,7 +356,7 @@ func TestQuoteFees_PopulatesFxUsed(t *testing.T) {
plan.OrganizationRef = &orgRef plan.OrganizationRef = &orgRef
fakeOracle := &oracleclient.Fake{ fakeOracle := &oracleclient.Fake{
LatestRateFn: func(_ context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) { LatestRateFn: func(ctx context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) {
return &oracleclient.RateSnapshot{ return &oracleclient.RateSnapshot{
Pair: req.Pair, Pair: req.Pair,
Mid: "1.2300", Mid: "1.2300",
@@ -412,12 +399,10 @@ func TestQuoteFees_PopulatesFxUsed(t *testing.T) {
if resp.GetFxUsed() == nil { if resp.GetFxUsed() == nil {
t.Fatalf("expected FxUsed to be populated") t.Fatalf("expected FxUsed to be populated")
} }
fx := resp.GetFxUsed() fx := resp.GetFxUsed()
if fx.GetProvider() != "TestProvider" || fx.GetRate().GetValue() != "1.2300" { if fx.GetProvider() != "TestProvider" || fx.GetRate().GetValue() != "1.2300" {
t.Fatalf("unexpected FxUsed payload: %+v", fx) t.Fatalf("unexpected FxUsed payload: %+v", fx)
} }
if fx.GetPair().GetBase() != "USD" || fx.GetPair().GetQuote() != "EUR" { if fx.GetPair().GetBase() != "USD" || fx.GetPair().GetQuote() != "EUR" {
t.Fatalf("unexpected currency pair: %+v", fx.GetPair()) t.Fatalf("unexpected currency pair: %+v", fx.GetPair())
} }
@@ -452,59 +437,49 @@ func (s *stubPlansStore) Get(context.Context, bson.ObjectID) (*model.FeePlan, er
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
func (s *stubPlansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) { func (s *stubPlansStore) GetActivePlan(_ context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
if !orgRef.IsZero() { if !orgRef.IsZero() {
if plan, err := s.FindActiveOrgPlan(ctx, orgRef, asOf); err == nil { if plan, err := s.FindActiveOrgPlan(context.Background(), orgRef, at); err == nil {
return plan, nil return plan, nil
} else if !errors.Is(err, storage.ErrFeePlanNotFound) { } else if !errors.Is(err, storage.ErrFeePlanNotFound) {
return nil, err return nil, err
} }
} }
return s.FindActiveGlobalPlan(context.Background(), at)
return s.FindActiveGlobalPlan(ctx, asOf)
} }
func (s *stubPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) { func (s *stubPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
if s.plan == nil { if s.plan == nil {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if (s.plan.OrganizationRef != nil) && (*s.plan.OrganizationRef != orgRef) { if (s.plan.OrganizationRef != nil) && (*s.plan.OrganizationRef != orgRef) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if !s.plan.Active { if !s.plan.Active {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.plan.EffectiveFrom.After(at) {
if s.plan.EffectiveFrom.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.plan.EffectiveTo != nil && !s.plan.EffectiveTo.After(at) {
if s.plan.EffectiveTo != nil && !s.plan.EffectiveTo.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return s.plan, nil return s.plan, nil
} }
func (s *stubPlansStore) FindActiveGlobalPlan(_ context.Context, asOf time.Time) (*model.FeePlan, error) { func (s *stubPlansStore) FindActiveGlobalPlan(_ context.Context, at time.Time) (*model.FeePlan, error) {
if s.globalPlan == nil { if s.globalPlan == nil {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if !s.globalPlan.Active { if !s.globalPlan.Active {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.globalPlan.EffectiveFrom.After(at) {
if s.globalPlan.EffectiveFrom.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.globalPlan.EffectiveTo != nil && !s.globalPlan.EffectiveTo.After(at) {
if s.globalPlan.EffectiveTo != nil && !s.globalPlan.EffectiveTo.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return s.globalPlan, nil return s.globalPlan, nil
} }
@@ -534,10 +509,8 @@ func (s *stubCalculator) Compute(_ context.Context, plan *model.FeePlan, _ *fees
s.called = true s.called = true
s.gotPlan = plan s.gotPlan = plan
s.bookedAt = bookedAt s.bookedAt = bookedAt
if s.err != nil { if s.err != nil {
return nil, s.err return nil, s.err
} }
return s.result, nil return s.result, nil
} }

View File

@@ -7,8 +7,6 @@ import (
func convertTrigger(trigger feesv1.Trigger) model.Trigger { func convertTrigger(trigger feesv1.Trigger) model.Trigger {
switch trigger { switch trigger {
case feesv1.Trigger_TRIGGER_UNSPECIFIED:
return model.TriggerUnspecified
case feesv1.Trigger_TRIGGER_CAPTURE: case feesv1.Trigger_TRIGGER_CAPTURE:
return model.TriggerCapture return model.TriggerCapture
case feesv1.Trigger_TRIGGER_REFUND: case feesv1.Trigger_TRIGGER_REFUND:

View File

@@ -28,13 +28,12 @@ const (
type FeePlan struct { type FeePlan struct {
storable.Base `bson:",inline" json:",inline"` storable.Base `bson:",inline" json:",inline"`
model.Describable `bson:",inline" json:",inline"` model.Describable `bson:",inline" json:",inline"`
OrganizationRef *bson.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"`
OrganizationRef *bson.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"` Active bool `bson:"active" json:"active"`
Active bool `bson:"active" json:"active"` EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"`
EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"` EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"`
EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"` Rules []FeeRule `bson:"rules,omitempty" json:"rules,omitempty"`
Rules []FeeRule `bson:"rules,omitempty" json:"rules,omitempty"` Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
} }
// Collection implements storable.Storable. // Collection implements storable.Storable.
@@ -44,21 +43,21 @@ func (*FeePlan) Collection() string {
// FeeRule represents a single pricing rule within a plan. // FeeRule represents a single pricing rule within a plan.
type FeeRule struct { type FeeRule struct {
RuleID string `bson:"ruleId" json:"ruleId"` RuleID string `bson:"ruleId" json:"ruleId"`
Trigger Trigger `bson:"trigger" json:"trigger"` Trigger Trigger `bson:"trigger" json:"trigger"`
Priority int `bson:"priority" json:"priority"` Priority int `bson:"priority" json:"priority"`
Percentage string `bson:"percentage,omitempty" json:"percentage,omitempty"` Percentage string `bson:"percentage,omitempty" json:"percentage,omitempty"`
FixedAmount string `bson:"fixedAmount,omitempty" json:"fixedAmount,omitempty"` FixedAmount string `bson:"fixedAmount,omitempty" json:"fixedAmount,omitempty"`
Currency string `bson:"currency,omitempty" json:"currency,omitempty"` Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
MinimumAmount string `bson:"minimumAmount,omitempty" json:"minimumAmount,omitempty"` MinimumAmount string `bson:"minimumAmount,omitempty" json:"minimumAmount,omitempty"`
MaximumAmount string `bson:"maximumAmount,omitempty" json:"maximumAmount,omitempty"` MaximumAmount string `bson:"maximumAmount,omitempty" json:"maximumAmount,omitempty"`
AppliesTo map[string]string `bson:"appliesTo,omitempty" json:"appliesTo,omitempty"` AppliesTo map[string]string `bson:"appliesTo,omitempty" json:"appliesTo,omitempty"`
Formula string `bson:"formula,omitempty" json:"formula,omitempty"` Formula string `bson:"formula,omitempty" json:"formula,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
LedgerAccountRef string `bson:"ledgerAccountRef,omitempty" json:"ledgerAccountRef,omitempty"` LedgerAccountRef string `bson:"ledgerAccountRef,omitempty" json:"ledgerAccountRef,omitempty"`
LineType string `bson:"lineType,omitempty" json:"lineType,omitempty"` LineType string `bson:"lineType,omitempty" json:"lineType,omitempty"`
EntrySide string `bson:"entrySide,omitempty" json:"entrySide,omitempty"` EntrySide string `bson:"entrySide,omitempty" json:"entrySide,omitempty"`
Rounding string `bson:"rounding,omitempty" json:"rounding,omitempty"` Rounding string `bson:"rounding,omitempty" json:"rounding,omitempty"`
EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"` EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"`
EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"` EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"`
} }

View File

@@ -43,22 +43,18 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
defer cancel() defer cancel()
if err := result.Ping(ctx); err != nil { if err := result.Ping(ctx); err != nil {
result.logger.Error("Mongo ping failed during store init", zap.Error(err)) result.logger.Error("mongo ping failed during store init", zap.Error(err))
return nil, err return nil, err
} }
plansStore, err := store.NewPlans(result.logger, database) plansStore, err := store.NewPlans(result.logger, database)
if err != nil { if err != nil {
result.logger.Error("Failed to initialise plans store", zap.Error(err)) result.logger.Error("failed to initialise plans store", zap.Error(err))
return nil, err return nil, err
} }
result.plans = plansStore result.plans = plansStore
result.logger.Info("Billing fees MongoDB storage initialised") result.logger.Info("Billing fees MongoDB storage initialised")
return result, nil return result, nil
} }

View File

@@ -28,8 +28,6 @@ type plansStore struct {
repo repository.Repository repo repository.Repository
} }
const maxActivePlanResults = 2
// NewPlans constructs a Mongo-backed PlansStore. // NewPlans constructs a Mongo-backed PlansStore.
func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, error) { func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, error) {
repo := repository.CreateMongoRepository(db, mservice.FeePlans) repo := repository.CreateMongoRepository(db, mservice.FeePlans)
@@ -42,8 +40,7 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
}, },
} }
if err := repo.CreateIndex(orgIndex); err != nil { if err := repo.CreateIndex(orgIndex); err != nil {
logger.Error("Failed to ensure fee plan organization index", zap.Error(err)) logger.Error("failed to ensure fee plan organization index", zap.Error(err))
return nil, err return nil, err
} }
@@ -56,8 +53,7 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
Unique: true, Unique: true,
} }
if err := repo.CreateIndex(uniqueIndex); err != nil { if err := repo.CreateIndex(uniqueIndex); err != nil {
logger.Error("Failed to ensure fee plan uniqueness index", zap.Error(err)) logger.Error("failed to ensure fee plan uniqueness index", zap.Error(err))
return nil, err return nil, err
} }
@@ -71,7 +67,7 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
}, },
} }
if err := repo.CreateIndex(activeIndex); err != nil { if err := repo.CreateIndex(activeIndex); err != nil {
logger.Warn("Failed to ensure fee plan active index", zap.Error(err)) logger.Warn("failed to ensure fee plan active index", zap.Error(err))
} }
return &plansStore{ return &plansStore{
@@ -84,7 +80,6 @@ func (p *plansStore) Create(ctx context.Context, plan *model.FeePlan) error {
if err := validatePlan(plan); err != nil { if err := validatePlan(plan); err != nil {
return err return err
} }
if err := p.ensureNoOverlap(ctx, plan); err != nil { if err := p.ensureNoOverlap(ctx, plan); err != nil {
return err return err
} }
@@ -93,12 +88,9 @@ func (p *plansStore) Create(ctx context.Context, plan *model.FeePlan) error {
if errors.Is(err, merrors.ErrDataConflict) { if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicateFeePlan return storage.ErrDuplicateFeePlan
} }
p.logger.Warn("failed to create fee plan", zap.Error(err))
p.logger.Warn("Failed to create fee plan", zap.Error(err))
return err return err
} }
return nil return nil
} }
@@ -106,21 +98,17 @@ func (p *plansStore) Update(ctx context.Context, plan *model.FeePlan) error {
if plan == nil || plan.GetID() == nil || plan.GetID().IsZero() { if plan == nil || plan.GetID() == nil || plan.GetID().IsZero() {
return merrors.InvalidArgument("plansStore: invalid fee plan reference") return merrors.InvalidArgument("plansStore: invalid fee plan reference")
} }
if err := validatePlan(plan); err != nil { if err := validatePlan(plan); err != nil {
return err return err
} }
if err := p.ensureNoOverlap(ctx, plan); err != nil { if err := p.ensureNoOverlap(ctx, plan); err != nil {
return err return err
} }
if err := p.repo.Update(ctx, plan); err != nil { if err := p.repo.Update(ctx, plan); err != nil {
p.logger.Warn("Failed to update fee plan", zap.Error(err)) p.logger.Warn("failed to update fee plan", zap.Error(err))
return err return err
} }
return nil return nil
} }
@@ -128,83 +116,72 @@ func (p *plansStore) Get(ctx context.Context, planRef bson.ObjectID) (*model.Fee
if planRef.IsZero() { if planRef.IsZero() {
return nil, merrors.InvalidArgument("plansStore: zero plan reference") return nil, merrors.InvalidArgument("plansStore: zero plan reference")
} }
result := &model.FeePlan{} result := &model.FeePlan{}
if err := p.repo.Get(ctx, planRef, result); err != nil { if err := p.repo.Get(ctx, planRef, result); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return nil, err return nil, err
} }
return result, nil return result, nil
} }
func (p *plansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) { func (p *plansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
// Compatibility shim: prefer org plan, fall back to global; allow zero org to mean global. // Compatibility shim: prefer org plan, fall back to global; allow zero org to mean global.
if orgRef.IsZero() { if orgRef.IsZero() {
return p.FindActiveGlobalPlan(ctx, asOf) return p.FindActiveGlobalPlan(ctx, at)
} }
plan, err := p.FindActiveOrgPlan(ctx, orgRef, asOf) plan, err := p.FindActiveOrgPlan(ctx, orgRef, at)
if err == nil { if err == nil {
return plan, nil return plan, nil
} }
if errors.Is(err, storage.ErrFeePlanNotFound) { if errors.Is(err, storage.ErrFeePlanNotFound) {
return p.FindActiveGlobalPlan(ctx, asOf) return p.FindActiveGlobalPlan(ctx, at)
} }
return nil, err return nil, err
} }
func (p *plansStore) FindActiveOrgPlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) { func (p *plansStore) FindActiveOrgPlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
if orgRef.IsZero() { if orgRef.IsZero() {
return nil, merrors.InvalidArgument("plansStore: zero organization reference") return nil, merrors.InvalidArgument("plansStore: zero organization reference")
} }
query := repository.Query().Filter(repository.OrgField(), orgRef) query := repository.Query().Filter(repository.OrgField(), orgRef)
return p.findActivePlan(ctx, query, at)
return p.findActivePlan(ctx, query, asOf)
} }
func (p *plansStore) FindActiveGlobalPlan(ctx context.Context, asOf time.Time) (*model.FeePlan, error) { func (p *plansStore) FindActiveGlobalPlan(ctx context.Context, at time.Time) (*model.FeePlan, error) {
globalQuery := repository.Query().Or( globalQuery := repository.Query().Or(
repository.Exists(repository.OrgField(), false), repository.Exists(repository.OrgField(), false),
repository.Query().Filter(repository.OrgField(), nil), repository.Query().Filter(repository.OrgField(), nil),
) )
return p.findActivePlan(ctx, globalQuery, at)
return p.findActivePlan(ctx, globalQuery, asOf)
} }
var _ storage.PlansStore = (*plansStore)(nil) var _ storage.PlansStore = (*plansStore)(nil)
func (p *plansStore) findActivePlan(ctx context.Context, orgQuery builder.Query, asOf time.Time) (*model.FeePlan, error) { func (p *plansStore) findActivePlan(ctx context.Context, orgQuery builder.Query, at time.Time) (*model.FeePlan, error) {
limit := int64(maxActivePlanResults) limit := int64(2)
query := orgQuery. query := orgQuery.
Filter(repository.Field("active"), true). Filter(repository.Field("active"), true).
Comparison(repository.Field("effectiveFrom"), builder.Lte, asOf). Comparison(repository.Field("effectiveFrom"), builder.Lte, at).
Sort(repository.Field("effectiveFrom"), false). Sort(repository.Field("effectiveFrom"), false).
Limit(&limit) Limit(&limit)
query = query.And( query = query.And(
repository.Query().Or( repository.Query().Or(
repository.Query().Filter(repository.Field("effectiveTo"), nil), repository.Query().Filter(repository.Field("effectiveTo"), nil),
repository.Query().Comparison(repository.Field("effectiveTo"), builder.Gte, asOf), repository.Query().Comparison(repository.Field("effectiveTo"), builder.Gte, at),
), ),
) )
var plans []*model.FeePlan var plans []*model.FeePlan
decoder := func(cursor *mongo.Cursor) error { decoder := func(cursor *mongo.Cursor) error {
target := &model.FeePlan{} target := &model.FeePlan{}
if err := cursor.Decode(target); err != nil { if err := cursor.Decode(target); err != nil {
return err return err
} }
plans = append(plans, target) plans = append(plans, target)
return nil return nil
} }
@@ -212,18 +189,15 @@ func (p *plansStore) findActivePlan(ctx context.Context, orgQuery builder.Query,
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return nil, err return nil, err
} }
if len(plans) == 0 { if len(plans) == 0 {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if len(plans) > 1 { if len(plans) > 1 {
return nil, storage.ErrConflictingFeePlans return nil, storage.ErrConflictingFeePlans
} }
return plans[0], nil return plans[0], nil
} }
@@ -231,61 +205,44 @@ func validatePlan(plan *model.FeePlan) error {
if plan == nil { if plan == nil {
return merrors.InvalidArgument("plansStore: nil fee plan") return merrors.InvalidArgument("plansStore: nil fee plan")
} }
if len(plan.Rules) == 0 { if len(plan.Rules) == 0 {
return merrors.InvalidArgument("plansStore: fee plan must contain at least one rule") return merrors.InvalidArgument("plansStore: fee plan must contain at least one rule")
} }
if plan.Active && plan.EffectiveTo != nil && plan.EffectiveTo.Before(plan.EffectiveFrom) { if plan.Active && plan.EffectiveTo != nil && plan.EffectiveTo.Before(plan.EffectiveFrom) {
return merrors.InvalidArgument("plansStore: effectiveTo cannot be before effectiveFrom") return merrors.InvalidArgument("plansStore: effectiveTo cannot be before effectiveFrom")
} }
// Ensure unique priority per (trigger, appliesTo) combination. // Ensure unique priority per (trigger, appliesTo) combination.
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, rule := range plan.Rules { for _, rule := range plan.Rules {
if err := validateRule(rule); err != nil { if strings.TrimSpace(rule.Percentage) != "" {
return err if _, err := dmath.RatFromString(rule.Percentage); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule percentage")
}
}
if strings.TrimSpace(rule.FixedAmount) != "" {
if _, err := dmath.RatFromString(rule.FixedAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule fixed amount")
}
}
if strings.TrimSpace(rule.MinimumAmount) != "" {
if _, err := dmath.RatFromString(rule.MinimumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule minimum amount")
}
}
if strings.TrimSpace(rule.MaximumAmount) != "" {
if _, err := dmath.RatFromString(rule.MaximumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule maximum amount")
}
} }
appliesKey := normalizeAppliesTo(rule.AppliesTo) appliesKey := normalizeAppliesTo(rule.AppliesTo)
priorityKey := fmt.Sprintf("%s|%d|%s", rule.Trigger, rule.Priority, appliesKey) priorityKey := fmt.Sprintf("%s|%d|%s", rule.Trigger, rule.Priority, appliesKey)
if _, ok := seen[priorityKey]; ok { if _, ok := seen[priorityKey]; ok {
return merrors.InvalidArgument("plansStore: duplicate priority for trigger/appliesTo") return merrors.InvalidArgument("plansStore: duplicate priority for trigger/appliesTo")
} }
seen[priorityKey] = struct{}{} seen[priorityKey] = struct{}{}
} }
return nil
}
func validateRule(rule model.FeeRule) error {
if strings.TrimSpace(rule.Percentage) != "" {
if _, err := dmath.RatFromString(rule.Percentage); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule percentage")
}
}
if strings.TrimSpace(rule.FixedAmount) != "" {
if _, err := dmath.RatFromString(rule.FixedAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule fixed amount")
}
}
if strings.TrimSpace(rule.MinimumAmount) != "" {
if _, err := dmath.RatFromString(rule.MinimumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule minimum amount")
}
}
if strings.TrimSpace(rule.MaximumAmount) != "" {
if _, err := dmath.RatFromString(rule.MaximumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule maximum amount")
}
}
return nil return nil
} }
@@ -293,19 +250,15 @@ func normalizeAppliesTo(applies map[string]string) string {
if len(applies) == 0 { if len(applies) == 0 {
return "" return ""
} }
keys := make([]string, 0, len(applies)) keys := make([]string, 0, len(applies))
for k := range applies { for k := range applies {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Strings(keys) sort.Strings(keys)
parts := make([]string, 0, len(keys)) parts := make([]string, 0, len(keys))
for _, k := range keys { for _, k := range keys {
parts = append(parts, k+"="+normalizeAppliesToValue(applies[k])) parts = append(parts, k+"="+normalizeAppliesToValue(applies[k]))
} }
return strings.Join(parts, ",") return strings.Join(parts, ",")
} }
@@ -319,37 +272,28 @@ func normalizeAppliesToValue(value string) string {
seen := make(map[string]struct{}, len(values)) seen := make(map[string]struct{}, len(values))
normalized := make([]string, 0, len(values)) normalized := make([]string, 0, len(values))
hasWildcard := false hasWildcard := false
for _, value := range values { for _, value := range values {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
continue continue
} }
if value == "*" { if value == "*" {
hasWildcard = true hasWildcard = true
continue continue
} }
if _, ok := seen[value]; ok { if _, ok := seen[value]; ok {
continue continue
} }
seen[value] = struct{}{} seen[value] = struct{}{}
normalized = append(normalized, value) normalized = append(normalized, value)
} }
if hasWildcard { if hasWildcard {
return "*" return "*"
} }
if len(normalized) == 0 { if len(normalized) == 0 {
return "" return ""
} }
sort.Strings(normalized) sort.Strings(normalized)
return strings.Join(normalized, ",") return strings.Join(normalized, ",")
} }
@@ -358,7 +302,7 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
return nil return nil
} }
var orgQuery builder.Query orgQuery := repository.Query()
if plan.OrganizationRef.IsZero() { if plan.OrganizationRef.IsZero() {
orgQuery = repository.Query().Or( orgQuery = repository.Query().Or(
repository.Exists(repository.OrgField(), false), repository.Exists(repository.OrgField(), false),
@@ -370,7 +314,6 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
maxTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) maxTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
newFrom := plan.EffectiveFrom newFrom := plan.EffectiveFrom
newTo := maxTime newTo := maxTime
if plan.EffectiveTo != nil { if plan.EffectiveTo != nil {
newTo = *plan.EffectiveTo newTo = *plan.EffectiveTo
@@ -392,10 +335,8 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
query = query.Limit(&limit) query = query.Limit(&limit)
var overlapFound bool var overlapFound bool
decoder := func(cursor *mongo.Cursor) error {
decoder := func(_ *mongo.Cursor) error {
overlapFound = true overlapFound = true
return nil return nil
} }
@@ -403,13 +344,10 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil return nil
} }
return err return err
} }
if overlapFound { if overlapFound {
return storage.ErrConflictingFeePlans return storage.ErrConflictingFeePlans
} }
return nil return nil
} }

View File

@@ -1,196 +0,0 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- wsl
- wrapcheck
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- funlen
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

View File

@@ -1,11 +1,11 @@
module github.com/tech/sendico/discovery module github.com/tech/sendico/discovery
go 1.25.7 go 1.25.6
replace github.com/tech/sendico/pkg => ../pkg replace github.com/tech/sendico/pkg => ../pkg
require ( require (
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.4
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
@@ -20,17 +20,17 @@ require (
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -38,12 +38,12 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -38,8 +38,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -152,16 +152,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/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 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -191,16 +191,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,10 +208,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -1,4 +1,3 @@
// Package appversion exposes build-time version information for the discovery service.
package appversion package appversion
import ( import (
@@ -15,9 +14,8 @@ var (
BuildDate string BuildDate string
) )
// Create returns a version printer populated with the compile-time build metadata. func Create() version.Printer {
func Create() version.Printer { //nolint:ireturn // factory returns interface by design vi := version.Info{
info := version.Info{
Program: "Sendico Discovery Service", Program: "Sendico Discovery Service",
Revision: Revision, Revision: Revision,
Branch: Branch, Branch: Branch,
@@ -25,6 +23,5 @@ func Create() version.Printer { //nolint:ireturn // factory returns interface by
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&vi)
return vf.Create(&info)
} }

View File

@@ -1,4 +1,3 @@
// Package serverimp contains the concrete discovery server implementation.
package serverimp package serverimp
import ( import (
@@ -11,10 +10,7 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const ( const defaultMetricsAddress = ":9405"
defaultMetricsAddress = ":9405"
defaultShutdownTimeoutSeconds = 15
)
type config struct { type config struct {
Runtime *grpcapp.RuntimeConfig `yaml:"runtime"` Runtime *grpcapp.RuntimeConfig `yaml:"runtime"`
@@ -28,28 +24,24 @@ type metricsConfig struct {
} }
type registryConfig struct { type registryConfig struct {
KVTTLSeconds *int `yaml:"kv_ttl_seconds"` //nolint:tagliatelle // matches config file format KVTTLSeconds *int `yaml:"kv_ttl_seconds"`
} }
func (i *Imp) loadConfig() (*config, error) { func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err
return nil, err //nolint:wrapcheck
} }
cfg := &config{} cfg := &config{}
if err := yaml.Unmarshal(data, cfg); err != nil {
err = yaml.Unmarshal(data, cfg)
if err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err
return nil, err //nolint:wrapcheck
} }
if cfg.Runtime == nil { if cfg.Runtime == nil {
cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: defaultShutdownTimeoutSeconds} cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15}
} }
if cfg.Metrics != nil && strings.TrimSpace(cfg.Metrics.Address) == "" { if cfg.Metrics != nil && strings.TrimSpace(cfg.Metrics.Address) == "" {

View File

@@ -14,51 +14,43 @@ import (
func (i *Imp) startDiscovery(cfg *config) error { func (i *Imp) startDiscovery(cfg *config) error {
if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" { if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" {
//nolint:wrapcheck
return merrors.InvalidArgument("discovery service: messaging configuration is required", "messaging") return merrors.InvalidArgument("discovery service: messaging configuration is required", "messaging")
} }
broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging) broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging)
if err != nil { if err != nil {
return err //nolint:wrapcheck return err
} }
i.logger.Info("Discovery messaging broker ready", zap.String("messaging_driver", string(cfg.Messaging.Driver))) i.logger.Info("Discovery messaging broker ready", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker) producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker)
registry := discovery.NewRegistry() registry := discovery.NewRegistry()
var registryOpts []discovery.RegistryOption var registryOpts []discovery.RegistryOption
if cfg.Registry != nil && cfg.Registry.KVTTLSeconds != nil { if cfg.Registry != nil && cfg.Registry.KVTTLSeconds != nil {
ttlSeconds := *cfg.Registry.KVTTLSeconds ttlSeconds := *cfg.Registry.KVTTLSeconds
if ttlSeconds < 0 { if ttlSeconds < 0 {
i.logger.Warn("Discovery registry TTL is negative, disabling TTL", zap.Int("ttl_seconds", ttlSeconds)) i.logger.Warn("Discovery registry TTL is negative, disabling TTL", zap.Int("ttl_seconds", ttlSeconds))
ttlSeconds = 0 ttlSeconds = 0
} }
registryOpts = append(registryOpts, discovery.WithRegistryKVTTL(time.Duration(ttlSeconds)*time.Second)) registryOpts = append(registryOpts, discovery.WithRegistryKVTTL(time.Duration(ttlSeconds)*time.Second))
} }
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, string(mservice.Discovery), registryOpts...)
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, mservice.Discovery, registryOpts...)
if err != nil { if err != nil {
return err //nolint:wrapcheck return err
} }
svc.Start() svc.Start()
i.registrySvc = svc i.registrySvc = svc
announce := discovery.Announcement{ announce := discovery.Announcement{
Service: "DISCOVERY", Service: "DISCOVERY",
InstanceID: discovery.InstanceID(), InstanceID: discovery.InstanceID(),
Operations: []string{discovery.OperationDiscoveryLookup}, Operations: []string{"discovery.lookup"},
Version: appversion.Create().Short(), Version: appversion.Create().Short(),
} }
i.announcer = discovery.NewAnnouncer(i.logger, producer, mservice.Discovery, announce) i.announcer = discovery.NewAnnouncer(i.logger, producer, string(mservice.Discovery), announce)
i.announcer.Start() i.announcer.Start()
i.logger.Info("Discovery registry service started", zap.String("messaging_driver", string(cfg.Messaging.Driver))) i.logger.Info("Discovery registry service started", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
return nil return nil
} }
@@ -66,12 +58,10 @@ func (i *Imp) stopDiscovery() {
if i == nil { if i == nil {
return return
} }
if i.announcer != nil { if i.announcer != nil {
i.announcer.Stop() i.announcer.Stop()
i.announcer = nil i.announcer = nil
} }
if i.registrySvc != nil { if i.registrySvc != nil {
i.registrySvc.Stop() i.registrySvc.Stop()
i.registrySvc = nil i.registrySvc = nil

View File

@@ -15,30 +15,22 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
const readHeaderTimeout = 5 * time.Second
func (i *Imp) startMetrics(cfg *metricsConfig) { func (i *Imp) startMetrics(cfg *metricsConfig) {
if i == nil { if i == nil {
return return
} }
address := "" address := ""
if cfg != nil { if cfg != nil {
address = strings.TrimSpace(cfg.Address) address = strings.TrimSpace(cfg.Address)
} }
if address == "" { if address == "" {
i.logger.Info("Metrics endpoint disabled") i.logger.Info("Metrics endpoint disabled")
return return
} }
lc := net.ListenConfig{} listener, err := net.Listen("tcp", address)
listener, err := lc.Listen(context.Background(), "tcp", address)
if err != nil { if err != nil {
i.logger.Error("Failed to bind metrics listener", zap.String("address", address), zap.Error(err)) i.logger.Error("Failed to bind metrics listener", zap.String("address", address), zap.Error(err))
return return
} }
@@ -46,9 +38,7 @@ func (i *Imp) startMetrics(cfg *metricsConfig) {
router.Handle("/metrics", promhttp.Handler()) router.Handle("/metrics", promhttp.Handler())
var healthRouter routers.Health var healthRouter routers.Health
if hr, err := routers.NewHealthRouter(i.logger.Named("metrics"), router, ""); err != nil {
hr, err := routers.NewHealthRouter(i.logger.Named("metrics"), router, "")
if err != nil {
i.logger.Warn("Failed to initialise health router", zap.Error(err)) i.logger.Warn("Failed to initialise health router", zap.Error(err))
} else { } else {
hr.SetStatus(health.SSStarting) hr.SetStatus(health.SSStarting)
@@ -59,16 +49,13 @@ func (i *Imp) startMetrics(cfg *metricsConfig) {
i.metricsSrv = &http.Server{ i.metricsSrv = &http.Server{
Addr: address, Addr: address,
Handler: router, Handler: router,
ReadHeaderTimeout: readHeaderTimeout, ReadHeaderTimeout: 5 * time.Second,
} }
go func() { go func() {
i.logger.Info("Prometheus endpoint listening", zap.String("address", address)) i.logger.Info("Prometheus endpoint listening", zap.String("address", address))
if err := i.metricsSrv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
serveErr := i.metricsSrv.Serve(listener) i.logger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err))
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
i.logger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(serveErr))
if healthRouter != nil { if healthRouter != nil {
healthRouter.SetStatus(health.SSTerminating) healthRouter.SetStatus(health.SSTerminating)
} }
@@ -82,18 +69,14 @@ func (i *Imp) shutdownMetrics(ctx context.Context) {
i.metricsHealth.Finish() i.metricsHealth.Finish()
i.metricsHealth = nil i.metricsHealth = nil
} }
if i.metricsSrv == nil { if i.metricsSrv == nil {
return return
} }
if err := i.metricsSrv.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
err := i.metricsSrv.Shutdown(ctx)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
i.logger.Warn("Failed to stop metrics server", zap.Error(err)) i.logger.Warn("Failed to stop metrics server", zap.Error(err))
} else { } else {
i.logger.Info("Metrics server stopped") i.logger.Info("Metrics server stopped")
} }
i.metricsSrv = nil i.metricsSrv = nil
} }
@@ -101,6 +84,5 @@ func (i *Imp) setMetricsStatus(status health.ServiceStatus) {
if i == nil || i.metricsHealth == nil { if i == nil || i.metricsHealth == nil {
return return
} }
i.metricsHealth.SetStatus(status) i.metricsHealth.SetStatus(status)
} }

View File

@@ -10,9 +10,6 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
const defaultShutdownTimeout = 15 * time.Second
// Create returns a new server implementation configured with the given logger, config file, and debug flag.
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
return &Imp{ return &Imp{
logger: logger.Named("server"), logger: logger.Named("server"),
@@ -21,7 +18,6 @@ func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
}, nil }, nil
} }
// Start loads configuration, starts metrics and the discovery registry, then blocks until stopped.
func (i *Imp) Start() error { func (i *Imp) Start() error {
i.initStopChannels() i.initStopChannels()
defer i.closeDone() defer i.closeDone()
@@ -32,37 +28,29 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.config = cfg i.config = cfg
messagingDriver := "none" messagingDriver := "none"
if cfg.Messaging != nil { if cfg.Messaging != nil {
messagingDriver = string(cfg.Messaging.Driver) messagingDriver = string(cfg.Messaging.Driver)
} }
metricsAddress := "" metricsAddress := ""
if cfg.Metrics != nil { if cfg.Metrics != nil {
metricsAddress = strings.TrimSpace(cfg.Metrics.Address) metricsAddress = strings.TrimSpace(cfg.Metrics.Address)
} }
if metricsAddress == "" { if metricsAddress == "" {
metricsAddress = "disabled" metricsAddress = "disabled"
} }
i.logger.Info("Discovery config loaded", zap.String("messaging_driver", messagingDriver), zap.String("metrics_address", metricsAddress))
i.logger.Info("Discovery config loaded",
zap.String("messaging_driver", messagingDriver),
zap.String("metrics_address", metricsAddress))
i.startMetrics(cfg.Metrics) i.startMetrics(cfg.Metrics)
err = i.startDiscovery(cfg) if err := i.startDiscovery(cfg); err != nil {
if err != nil {
i.stopDiscovery() i.stopDiscovery()
i.setMetricsStatus(health.SSTerminating) i.setMetricsStatus(health.SSTerminating)
ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout()) ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout())
i.shutdownMetrics(ctx) i.shutdownMetrics(ctx)
cancel() cancel()
return err return err
} }
@@ -71,11 +59,9 @@ func (i *Imp) Start() error {
<-i.stopCh <-i.stopCh
i.logger.Info("Discovery service stop signal received") i.logger.Info("Discovery service stop signal received")
return nil return nil
} }
// Shutdown gracefully stops the discovery service and its metrics server.
func (i *Imp) Shutdown() { func (i *Imp) Shutdown() {
timeout := i.shutdownTimeout() timeout := i.shutdownTimeout()
i.logger.Info("Stopping discovery service", zap.Duration("timeout", timeout)) i.logger.Info("Stopping discovery service", zap.Duration("timeout", timeout))
@@ -86,7 +72,6 @@ func (i *Imp) Shutdown() {
if i.doneCh != nil { if i.doneCh != nil {
<-i.doneCh <-i.doneCh
} }
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
i.shutdownMetrics(ctx) i.shutdownMetrics(ctx)
cancel() cancel()
@@ -98,7 +83,6 @@ func (i *Imp) initStopChannels() {
if i.stopCh == nil { if i.stopCh == nil {
i.stopCh = make(chan struct{}) i.stopCh = make(chan struct{})
} }
if i.doneCh == nil { if i.doneCh == nil {
i.doneCh = make(chan struct{}) i.doneCh = make(chan struct{})
} }
@@ -124,6 +108,5 @@ func (i *Imp) shutdownTimeout() time.Duration {
if i.config != nil && i.config.Runtime != nil { if i.config != nil && i.config.Runtime != nil {
return i.config.Runtime.ShutdownTimeout() return i.config.Runtime.ShutdownTimeout()
} }
return 15 * time.Second
return defaultShutdownTimeout
} }

View File

@@ -9,7 +9,6 @@ import (
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
) )
// Imp is the concrete implementation of the discovery server application.
type Imp struct { type Imp struct {
logger mlogger.Logger logger mlogger.Logger
file string file string

View File

@@ -1,4 +1,3 @@
// Package server provides the discovery service application factory.
package server package server
import ( import (
@@ -7,9 +6,6 @@ import (
"github.com/tech/sendico/pkg/server" "github.com/tech/sendico/pkg/server"
) )
// Create initialises and returns a new discovery server application.
//
//nolint:ireturn // factory returns interface by design
func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
return serverimp.Create(logger, file, debug) //nolint:wrapcheck return serverimp.Create(logger, file, debug)
} }

View File

@@ -1,4 +1,3 @@
// Package main is the entry point for the discovery service.
package main package main
import ( import (
@@ -9,9 +8,8 @@ import (
smain "github.com/tech/sendico/pkg/server/main" smain "github.com/tech/sendico/pkg/server/main"
) )
//nolint:ireturn // factory returns interface by design
func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
return si.Create(logger, file, debug) //nolint:wrapcheck return si.Create(logger, file, debug)
} }
func main() { func main() {

View File

@@ -1,196 +0,0 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- wrapcheck
- wsl
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- funlen
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

View File

@@ -19,10 +19,10 @@ market:
quote: "EUR" quote: "EUR"
symbol: "EURUSDT" symbol: "EURUSDT"
invert: true invert: true
- base: "USDT" - base: "USD"
quote: "USD" quote: "USDT"
symbol: "USDTUSD" symbol: "USDTUSD"
invert: false invert: true
- base: "UAH" - base: "UAH"
quote: "USDT" quote: "USDT"
symbol: "USDTUAH" symbol: "USDTUAH"

View File

@@ -19,10 +19,10 @@ market:
quote: "EUR" quote: "EUR"
symbol: "EURUSDT" symbol: "EURUSDT"
invert: true invert: true
- base: "USDT" - base: "USD"
quote: "USD" quote: "USDT"
symbol: "USDTUSD" symbol: "USDTUSD"
invert: false invert: true
- base: "UAH" - base: "UAH"
quote: "USDT" quote: "USDT"
symbol: "USDTUAH" symbol: "USDTUAH"

View File

@@ -1,19 +1,19 @@
module github.com/tech/sendico/fx/ingestor module github.com/tech/sendico/fx/ingestor
go 1.25.7 go 1.25.6
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/fx/storage => ../storage replace github.com/tech/sendico/fx/storage => ../storage
require ( require (
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.4
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/tech/sendico/fx/storage v0.0.0 github.com/tech/sendico/fx/storage v0.0.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
golang.org/x/net v0.51.0 golang.org/x/net v0.49.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -25,17 +25,17 @@ require (
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -43,11 +43,11 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -38,8 +38,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -152,16 +152,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/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 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -191,16 +191,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,10 +208,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -39,7 +39,6 @@ func New(logger mlogger.Logger, cfgPath string) (*App, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &App{ return &App{
logger: logger, logger: logger,
cfg: cfg, cfg: cfg,
@@ -51,24 +50,20 @@ func (a *App) Run(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
a.logger.Debug("Metrics server initialised") a.logger.Debug("Metrics server initialised")
defer metricsSrv.Close(context.Background())
defer metricsSrv.Close(context.Background()) //nolint:contextcheck conn, err := db.ConnectMongo(a.logger, a.cfg.Database)
conn, err := db.ConnectMongo(a.logger, a.cfg.Database) //nolint:contextcheck
if err != nil { if err != nil {
return err return err
} }
defer conn.Disconnect(context.Background()) //nolint:errcheck,contextcheck defer conn.Disconnect(context.Background())
a.logger.Debug("MongoDB connection established") a.logger.Debug("MongoDB connection established")
repo, err := mongostorage.New(a.logger, conn) //nolint:contextcheck repo, err := mongostorage.New(a.logger, conn)
if err != nil { if err != nil {
return err return err
} }
a.logger.Debug("Storage repository initialised") a.logger.Debug("Storage repository initialised")
service, err := ingestor.New(a.logger, a.cfg, repo) service, err := ingestor.New(a.logger, a.cfg, repo)
@@ -77,7 +72,6 @@ func (a *App) Run(ctx context.Context) error {
} }
var announcer *discovery.Announcer var announcer *discovery.Announcer
if cfg := a.cfg.Messaging; cfg != nil && cfg.Driver != "" { if cfg := a.cfg.Messaging; cfg != nil && cfg.Driver != "" {
broker, err := msg.CreateMessagingBroker(a.logger.Named("discovery_bus"), cfg) broker, err := msg.CreateMessagingBroker(a.logger.Named("discovery_bus"), cfg)
if err != nil { if err != nil {
@@ -86,11 +80,10 @@ func (a *App) Run(ctx context.Context) error {
producer := msgproducer.NewProducer(a.logger.Named("discovery_producer"), broker) producer := msgproducer.NewProducer(a.logger.Named("discovery_producer"), broker)
announce := discovery.Announcement{ announce := discovery.Announcement{
Service: "FX_INGESTOR", Service: "FX_INGESTOR",
Operations: []string{discovery.OperationFXIngest}, Operations: []string{"fx.ingest"},
Version: appversion.Create().Short(), Version: appversion.Create().Short(),
} }
announcer = discovery.NewAnnouncer(a.logger, producer, "fx_ingestor", announce) announcer = discovery.NewAnnouncer(a.logger, producer, "fx_ingestor", announce)
announcer.Start() announcer.Start()
defer announcer.Stop() defer announcer.Stop()
} }
@@ -99,15 +92,12 @@ func (a *App) Run(ctx context.Context) error {
a.logger.Info("Starting FX ingestor service", zap.String("version", appversion.Create().Info())) a.logger.Info("Starting FX ingestor service", zap.String("version", appversion.Create().Info()))
metricsSrv.SetStatus(health.SSRunning) metricsSrv.SetStatus(health.SSRunning)
err = service.Run(ctx) if err := service.Run(ctx); err != nil {
if err != nil {
if !errors.Is(err, context.Canceled) { // ignore termination reques error if !errors.Is(err, context.Canceled) { // ignore termination reques error
a.logger.Error("Ingestor service exited with error", zap.Error(err)) a.logger.Error("Ingestor service exited with error", zap.Error(err))
} }
return err return err
} }
a.logger.Info("Ingestor service stopped") a.logger.Info("Ingestor service stopped")
return nil return nil
} }

View File

@@ -14,9 +14,8 @@ var (
BuildDate string BuildDate string
) )
//nolint:ireturn
func Create() version.Printer { func Create() version.Printer {
info := version.Info{ vi := version.Info{
Program: "Sendico FX Ingestor Service", Program: "Sendico FX Ingestor Service",
Revision: Revision, Revision: Revision,
Branch: Branch, Branch: Branch,
@@ -24,6 +23,5 @@ func Create() version.Printer {
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&vi)
return vf.Create(&info)
} }

View File

@@ -15,7 +15,7 @@ import (
const defaultPollInterval = 30 * time.Second const defaultPollInterval = 30 * time.Second
type Config struct { type Config struct {
PollIntervalSeconds int `yaml:"poll_interval_seconds"` //nolint:tagliatelle // matches config file format PollIntervalSeconds int `yaml:"poll_interval_seconds"`
Market MarketConfig `yaml:"market"` Market MarketConfig `yaml:"market"`
Database *db.Config `yaml:"database"` Database *db.Config `yaml:"database"`
Metrics *MetricsConfig `yaml:"metrics"` Metrics *MetricsConfig `yaml:"metrics"`
@@ -25,7 +25,6 @@ type Config struct {
pairsBySource map[mmodel.Driver][]PairConfig pairsBySource map[mmodel.Driver][]PairConfig
} }
//nolint:cyclop,funlen
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
if path == "" { if path == "" {
return nil, merrors.InvalidArgument("config: path is empty") return nil, merrors.InvalidArgument("config: path is empty")
@@ -37,23 +36,19 @@ func Load(path string) (*Config, error) {
} }
cfg := &Config{} cfg := &Config{}
if err := yaml.Unmarshal(data, cfg); err != nil {
err = yaml.Unmarshal(data, cfg)
if err != nil {
return nil, merrors.InternalWrap(err, "config: failed to parse yaml") return nil, merrors.InternalWrap(err, "config: failed to parse yaml")
} }
if len(cfg.Market.Sources) == 0 { if len(cfg.Market.Sources) == 0 {
return nil, merrors.InvalidArgument("config: no market sources configured") return nil, merrors.InvalidArgument("config: no market sources configured")
} }
sourceSet := make(map[mmodel.Driver]struct{}, len(cfg.Market.Sources)) sourceSet := make(map[mmodel.Driver]struct{}, len(cfg.Market.Sources))
for idx := range cfg.Market.Sources { for idx := range cfg.Market.Sources {
src := &cfg.Market.Sources[idx] src := &cfg.Market.Sources[idx]
if src.Driver.IsEmpty() { if src.Driver.IsEmpty() {
return nil, merrors.InvalidArgument("config: market source driver is empty") return nil, merrors.InvalidArgument("config: market source driver is empty")
} }
sourceSet[src.Driver] = struct{}{} sourceSet[src.Driver] = struct{}{}
} }
@@ -63,7 +58,6 @@ func Load(path string) (*Config, error) {
normalizedPairs := make(map[string][]PairConfig, len(cfg.Market.Pairs)) normalizedPairs := make(map[string][]PairConfig, len(cfg.Market.Pairs))
pairsBySource := make(map[mmodel.Driver][]PairConfig, len(cfg.Market.Pairs)) pairsBySource := make(map[mmodel.Driver][]PairConfig, len(cfg.Market.Pairs))
var flattened []Pair var flattened []Pair
for rawSource, pairList := range cfg.Market.Pairs { for rawSource, pairList := range cfg.Market.Pairs {
@@ -71,10 +65,8 @@ func Load(path string) (*Config, error) {
if driver.IsEmpty() { if driver.IsEmpty() {
return nil, merrors.InvalidArgument("config: pair source is empty") return nil, merrors.InvalidArgument("config: pair source is empty")
} }
if _, ok := sourceSet[driver]; !ok { if _, ok := sourceSet[driver]; !ok {
return nil, merrors.InvalidArgument( //nolint:lll return nil, merrors.InvalidArgument("config: pair references unknown source: "+driver.String(), "pairs."+driver.String())
"config: pair references unknown source: "+driver.String(), "pairs."+driver.String())
} }
processed := make([]PairConfig, len(pairList)) processed := make([]PairConfig, len(pairList))
@@ -82,25 +74,19 @@ func Load(path string) (*Config, error) {
pair := pairList[idx] pair := pairList[idx]
pair.Base = strings.ToUpper(strings.TrimSpace(pair.Base)) pair.Base = strings.ToUpper(strings.TrimSpace(pair.Base))
pair.Quote = strings.ToUpper(strings.TrimSpace(pair.Quote)) pair.Quote = strings.ToUpper(strings.TrimSpace(pair.Quote))
pair.Symbol = strings.TrimSpace(pair.Symbol) pair.Symbol = strings.TrimSpace(pair.Symbol)
if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" { if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" {
return nil, merrors.InvalidArgument( //nolint:lll return nil, merrors.InvalidArgument("config: pair entries must define base, quote, and symbol", "pairs."+driver.String())
"config: pair entries must define base, quote, and symbol", "pairs."+driver.String())
} }
if strings.TrimSpace(pair.Provider) == "" { if strings.TrimSpace(pair.Provider) == "" {
pair.Provider = strings.ToLower(driver.String()) pair.Provider = strings.ToLower(driver.String())
} }
processed[idx] = pair processed[idx] = pair
flattened = append(flattened, Pair{ flattened = append(flattened, Pair{
PairConfig: pair, PairConfig: pair,
Source: driver, Source: driver,
}) })
} }
pairsBySource[driver] = processed pairsBySource[driver] = processed
normalizedPairs[driver.String()] = processed normalizedPairs[driver.String()] = processed
} }
@@ -108,7 +94,6 @@ func Load(path string) (*Config, error) {
cfg.Market.Pairs = normalizedPairs cfg.Market.Pairs = normalizedPairs
cfg.pairsBySource = pairsBySource cfg.pairsBySource = pairsBySource
cfg.pairs = flattened cfg.pairs = flattened
if cfg.Database == nil { if cfg.Database == nil {
return nil, merrors.InvalidArgument("config: database configuration is required") return nil, merrors.InvalidArgument("config: database configuration is required")
} }
@@ -127,11 +112,9 @@ func (c *Config) PollInterval() time.Duration {
if c == nil { if c == nil {
return defaultPollInterval return defaultPollInterval
} }
if c.PollIntervalSeconds <= 0 { if c.PollIntervalSeconds <= 0 {
return defaultPollInterval return defaultPollInterval
} }
return time.Duration(c.PollIntervalSeconds) * time.Second return time.Duration(c.PollIntervalSeconds) * time.Second
} }
@@ -139,10 +122,8 @@ func (c *Config) Pairs() []Pair {
if c == nil { if c == nil {
return nil return nil
} }
out := make([]Pair, len(c.pairs)) out := make([]Pair, len(c.pairs))
copy(out, c.pairs) copy(out, c.pairs)
return out return out
} }
@@ -150,14 +131,12 @@ func (c *Config) PairsBySource() map[mmodel.Driver][]PairConfig {
if c == nil { if c == nil {
return nil return nil
} }
out := make(map[mmodel.Driver][]PairConfig, len(c.pairsBySource)) out := make(map[mmodel.Driver][]PairConfig, len(c.pairsBySource))
for driver, pairs := range c.pairsBySource { for driver, pairs := range c.pairsBySource {
cp := make([]PairConfig, len(pairs)) cp := make([]PairConfig, len(pairs))
copy(cp, pairs) copy(cp, pairs)
out[driver] = cp out[driver] = cp
} }
return out return out
} }
@@ -165,8 +144,6 @@ func (c *Config) MetricsConfig() *MetricsConfig {
if c == nil || c.Metrics == nil { if c == nil || c.Metrics == nil {
return nil return nil
} }
cp := *c.Metrics cp := *c.Metrics
return &cp return &cp
} }

View File

@@ -15,8 +15,7 @@ type PairConfig struct {
type Pair struct { type Pair struct {
PairConfig `yaml:",inline"` PairConfig `yaml:",inline"`
Source mmodel.Driver `yaml:"-"`
Source mmodel.Driver `yaml:"-"`
} }
type MarketConfig struct { type MarketConfig struct {

View File

@@ -28,11 +28,9 @@ func New(logger mlogger.Logger, cfg *config.Config, repo storage.Repository) (*S
if logger == nil { if logger == nil {
return nil, merrors.InvalidArgument("ingestor: nil logger") return nil, merrors.InvalidArgument("ingestor: nil logger")
} }
if cfg == nil { if cfg == nil {
return nil, merrors.InvalidArgument("ingestor: nil config") return nil, merrors.InvalidArgument("ingestor: nil config")
} }
if repo == nil { if repo == nil {
return nil, merrors.InvalidArgument("ingestor: nil repository") return nil, merrors.InvalidArgument("ingestor: nil repository")
} }
@@ -54,7 +52,6 @@ func New(logger mlogger.Logger, cfg *config.Config, repo storage.Repository) (*S
func (s *Service) Run(ctx context.Context) error { func (s *Service) Run(ctx context.Context) error {
interval := s.cfg.PollInterval() interval := s.cfg.PollInterval()
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
@@ -68,7 +65,6 @@ func (s *Service) Run(ctx context.Context) error {
select { select {
case <-ctx.Done(): case <-ctx.Done():
s.logger.Info("Context cancelled, stopping ingestor") s.logger.Info("Context cancelled, stopping ingestor")
return ctx.Err() return ctx.Err()
case <-ticker.C: case <-ticker.C:
if err := s.executePoll(ctx); err != nil { if err := s.executePoll(ctx); err != nil {
@@ -81,34 +77,27 @@ func (s *Service) Run(ctx context.Context) error {
func (s *Service) executePoll(ctx context.Context) error { func (s *Service) executePoll(ctx context.Context) error {
start := time.Now() start := time.Now()
err := s.pollOnce(ctx) err := s.pollOnce(ctx)
if s.metrics != nil { if s.metrics != nil {
s.metrics.observePoll(time.Since(start), err) s.metrics.observePoll(time.Since(start), err)
} }
return err return err
} }
func (s *Service) pollOnce(ctx context.Context) error { func (s *Service) pollOnce(ctx context.Context) error {
var firstErr error var firstErr error
failures := 0 failures := 0
for _, pair := range s.pairs { for _, pair := range s.pairs {
start := time.Now() start := time.Now()
err := s.upsertPair(ctx, pair) err := s.upsertPair(ctx, pair)
elapsed := time.Since(start) elapsed := time.Since(start)
if s.metrics != nil { if s.metrics != nil {
s.metrics.observePair(pair, elapsed, err) s.metrics.observePair(pair, elapsed, err)
} }
if err != nil { if err != nil {
if firstErr == nil { if firstErr == nil {
firstErr = err firstErr = err
} }
failures++ failures++
s.logger.Warn("Failed to ingest pair", s.logger.Warn("Failed to ingest pair",
zap.String("symbol", pair.Symbol), zap.String("symbol", pair.Symbol),
zap.String("source", pair.Source.String()), zap.String("source", pair.Source.String()),
@@ -121,17 +110,14 @@ func (s *Service) pollOnce(ctx context.Context) error {
) )
} }
} }
if failures > 0 { if failures > 0 {
s.logger.Warn("Ingestion poll completed with failures", zap.Int("failures", failures), zap.Int("total", len(s.pairs))) s.logger.Warn("Ingestion poll completed with failures", zap.Int("failures", failures), zap.Int("total", len(s.pairs)))
} else { } else {
s.logger.Debug("Ingestion poll completed", zap.Int("total", len(s.pairs))) s.logger.Debug("Ingestion poll completed", zap.Int("total", len(s.pairs)))
} }
return firstErr return firstErr
} }
//nolint:funlen
func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error { func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
connector, ok := s.connectors[pair.Source] connector, ok := s.connectors[pair.Source]
if !ok { if !ok {
@@ -147,7 +133,6 @@ func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
if err != nil { if err != nil {
return merrors.InvalidArgumentWrap(err, "parse bid price", "bid") return merrors.InvalidArgumentWrap(err, "parse bid price", "bid")
} }
ask, err := parseDecimal(ticker.AskPrice) ask, err := parseDecimal(ticker.AskPrice)
if err != nil { if err != nil {
return merrors.InvalidArgumentWrap(err, "parse ask price", "ask") return merrors.InvalidArgumentWrap(err, "parse ask price", "ask")
@@ -163,18 +148,16 @@ func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error {
} }
mid := new(big.Rat).Add(bid, ask) mid := new(big.Rat).Add(bid, ask)
mid.Quo(mid, big.NewRat(2, 1)) //nolint:mnd mid.Quo(mid, big.NewRat(2, 1))
spread := big.NewRat(0, 1) spread := big.NewRat(0, 1)
if mid.Sign() != 0 { if mid.Sign() != 0 {
spread.Sub(ask, bid) spread.Sub(ask, bid)
if spread.Sign() < 0 { if spread.Sign() < 0 {
spread.Neg(spread) spread.Neg(spread)
} }
spread.Quo(spread, mid) spread.Quo(spread, mid)
spread.Mul(spread, big.NewRat(10000, 1)) //nolint:mnd // basis points spread.Mul(spread, big.NewRat(10000, 1)) // basis points
} }
now := time.Now().UTC() now := time.Now().UTC()
@@ -218,7 +201,6 @@ func parseDecimal(value string) (*big.Rat, error) {
if _, ok := r.SetString(value); !ok { if _, ok := r.SetString(value); !ok {
return nil, merrors.InvalidArgument("invalid decimal \""+value+"\"", "value") return nil, merrors.InvalidArgument("invalid decimal \""+value+"\"", "value")
} }
return r, nil return r, nil
} }
@@ -226,11 +208,9 @@ func invertPrices(bid, ask *big.Rat) (*big.Rat, *big.Rat) {
if bid.Sign() == 0 || ask.Sign() == 0 { if bid.Sign() == 0 || ask.Sign() == 0 {
return bid, ask return bid, ask
} }
one := big.NewRat(1, 1) one := big.NewRat(1, 1)
invBid := new(big.Rat).Quo(one, ask) // invert ask to get bid invBid := new(big.Rat).Quo(one, ask) // invert ask to get bid
invAsk := new(big.Rat).Quo(one, bid) // invert bid to get ask invAsk := new(big.Rat).Quo(one, bid) // invert bid to get ask
return invBid, invAsk return invBid, invAsk
} }
@@ -238,7 +218,6 @@ func formatDecimal(r *big.Rat) string {
if r == nil { if r == nil {
return "0" return "0"
} }
// Format with 8 decimal places, trimming trailing zeros. // Format with 8 decimal places, trimming trailing zeros.
return r.FloatString(8) return r.FloatString(8)
} }

View File

@@ -27,33 +27,30 @@ type binanceConnector struct {
} }
const defaultBinanceBaseURL = "https://api.binance.com" const defaultBinanceBaseURL = "https://api.binance.com"
const ( const (
defaultDialTimeout = 5 * time.Second defaultDialTimeoutSeconds = 5 * time.Second
defaultDialKeepAlive = 30 * time.Second defaultDialKeepAliveSeconds = 30 * time.Second
defaultTLSHandshakeTimeout = 5 * time.Second defaultTLSHandshakeTimeoutSeconds = 5 * time.Second
defaultResponseHeaderTimeout = 10 * time.Second defaultResponseHeaderTimeoutSeconds = 10 * time.Second
defaultRequestTimeout = 10 * time.Second defaultRequestTimeoutSeconds = 10 * time.Second
) )
func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { //nolint:ireturn func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) {
baseURL := defaultBinanceBaseURL baseURL := defaultBinanceBaseURL
provider := strings.ToLower(mmodel.DriverBinance.String()) provider := strings.ToLower(mmodel.DriverBinance.String())
dialTimeout := defaultDialTimeout dialTimeout := defaultDialTimeoutSeconds
dialKeepAlive := defaultDialKeepAlive dialKeepAlive := defaultDialKeepAliveSeconds
tlsHandshakeTimeout := defaultTLSHandshakeTimeout tlsHandshakeTimeout := defaultTLSHandshakeTimeoutSeconds
responseHeaderTimeout := defaultResponseHeaderTimeout responseHeaderTimeout := defaultResponseHeaderTimeoutSeconds
requestTimeout := defaultRequestTimeout requestTimeout := defaultRequestTimeoutSeconds
if settings != nil { if settings != nil {
if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" { if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" {
baseURL = strings.TrimSpace(value) baseURL = strings.TrimSpace(value)
} }
if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" { if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" {
provider = strings.TrimSpace(value) provider = strings.TrimSpace(value)
} }
dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout) dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout)
dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive) dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive)
tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout) tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout)
@@ -99,7 +96,6 @@ func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmo
if err != nil { if err != nil {
return nil, merrors.InternalWrap(err, "binance: parse base url") return nil, merrors.InternalWrap(err, "binance: parse base url")
} }
endpoint.Path = "/api/v3/ticker/bookTicker" endpoint.Path = "/api/v3/ticker/bookTicker"
query := endpoint.Query() query := endpoint.Query()
query.Set("symbol", strings.ToUpper(strings.TrimSpace(symbol))) query.Set("symbol", strings.ToUpper(strings.TrimSpace(symbol)))
@@ -113,14 +109,12 @@ func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmo
resp, err := c.client.Do(req) resp, err := c.client.Do(req)
if err != nil { if err != nil {
c.logger.Warn("Binance request failed", zap.String("symbol", symbol), zap.Error(err)) c.logger.Warn("Binance request failed", zap.String("symbol", symbol), zap.Error(err))
return nil, merrors.InternalWrap(err, "binance: request failed") return nil, merrors.InternalWrap(err, "binance: request failed")
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode)) c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))
return nil, merrors.Internal("binance: unexpected status " + strconv.Itoa(resp.StatusCode)) return nil, merrors.Internal("binance: unexpected status " + strconv.Itoa(resp.StatusCode))
} }
@@ -130,11 +124,9 @@ func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmo
AskPrice string `json:"askPrice"` AskPrice string `json:"askPrice"`
} }
decodeErr := json.NewDecoder(resp.Body).Decode(&payload) if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
if decodeErr != nil { c.logger.Warn("Binance decode failed", zap.String("symbol", symbol), zap.Error(err))
c.logger.Warn("Binance decode failed", zap.String("symbol", symbol), zap.Error(decodeErr)) return nil, merrors.InternalWrap(err, "binance: decode response")
return nil, merrors.InternalWrap(decodeErr, "binance: decode response")
} }
return &mmodel.Ticker{ return &mmodel.Ticker{

View File

@@ -42,21 +42,21 @@ const (
) )
const ( const (
defaultDialTimeout = 5 * time.Second defaultDialTimeoutSeconds = 5 * time.Second
defaultDialKeepAlive = 30 * time.Second defaultDialKeepAliveSeconds = 30 * time.Second
defaultTLSHandshakeTimeout = 5 * time.Second defaultTLSHandshakeTimeoutSeconds = 5 * time.Second
defaultResponseHeaderTimeout = 10 * time.Second defaultResponseHeaderTimeoutSeconds = 10 * time.Second
defaultRequestTimeout = 10 * time.Second defaultRequestTimeoutSeconds = 10 * time.Second
) )
func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { //nolint:cyclop,funlen,nestif,ireturn func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) {
baseURL := defaultCBRBaseURL baseURL := defaultCBRBaseURL
provider := strings.ToLower(mmodel.DriverCBR.String()) provider := strings.ToLower(mmodel.DriverCBR.String())
dialTimeout := defaultDialTimeout dialTimeout := defaultDialTimeoutSeconds
dialKeepAlive := defaultDialKeepAlive dialKeepAlive := defaultDialKeepAliveSeconds
tlsHandshakeTimeout := defaultTLSHandshakeTimeout tlsHandshakeTimeout := defaultTLSHandshakeTimeoutSeconds
responseHeaderTimeout := defaultResponseHeaderTimeout responseHeaderTimeout := defaultResponseHeaderTimeoutSeconds
requestTimeout := defaultRequestTimeout requestTimeout := defaultRequestTimeoutSeconds
directoryPath := defaultDirectoryPath directoryPath := defaultDirectoryPath
dailyPath := defaultDailyPath dailyPath := defaultDailyPath
dynamicPath := defaultDynamicPath dynamicPath := defaultDynamicPath
@@ -79,11 +79,9 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
if value, ok := settings["dynamic_path"].(string); ok && strings.TrimSpace(value) != "" { if value, ok := settings["dynamic_path"].(string); ok && strings.TrimSpace(value) != "" {
dynamicPath = strings.TrimSpace(value) dynamicPath = strings.TrimSpace(value)
} }
if value, ok := settings["user_agent"].(string); ok && strings.TrimSpace(value) != "" { if value, ok := settings["user_agent"].(string); ok && strings.TrimSpace(value) != "" {
userAgent = strings.TrimSpace(value) userAgent = strings.TrimSpace(value)
} }
if value, ok := settings["accept_header"].(string); ok && strings.TrimSpace(value) != "" { if value, ok := settings["accept_header"].(string); ok && strings.TrimSpace(value) != "" {
acceptHeader = strings.TrimSpace(value) acceptHeader = strings.TrimSpace(value)
} }
@@ -134,8 +132,7 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
logger: logger.Named("cbr"), logger: logger.Named("cbr"),
} }
err = connector.refreshDirectory() if err := connector.refreshDirectory(); err != nil {
if err != nil {
return nil, err return nil, err
} }
@@ -168,7 +165,6 @@ func (c *cbrConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.
} }
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
return &mmodel.Ticker{ return &mmodel.Ticker{
Symbol: formatSymbol(isoCode, asOfDate), Symbol: formatSymbol(isoCode, asOfDate),
BidPrice: price, BidPrice: price,
@@ -217,9 +213,7 @@ func (c *cbrConnector) refreshDirectory() error {
decoder.CharsetReader = charset.NewReaderLabel decoder.CharsetReader = charset.NewReaderLabel
var directory valuteDirectory var directory valuteDirectory
if err := decoder.Decode(&directory); err != nil {
err = decoder.Decode(&directory)
if err != nil {
c.logger.Warn("CBR directory decode failed", zap.Error(err), zap.String("endpoint", endpoint)) c.logger.Warn("CBR directory decode failed", zap.Error(err), zap.String("endpoint", endpoint))
return merrors.InternalWrap(err, "cbr: decode directory") return merrors.InternalWrap(err, "cbr: decode directory")
} }
@@ -232,7 +226,6 @@ func (c *cbrConnector) refreshDirectory() error {
c.byISO = mapping.byISO c.byISO = mapping.byISO
c.byID = mapping.byID c.byID = mapping.byID
return nil return nil
} }
@@ -272,9 +265,7 @@ func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (s
decoder.CharsetReader = charset.NewReaderLabel decoder.CharsetReader = charset.NewReaderLabel
var payload dailyRates var payload dailyRates
if err := decoder.Decode(&payload); err != nil {
err = decoder.Decode(&payload)
if err != nil {
c.logger.Warn("CBR daily decode failed", zap.Error(err), c.logger.Warn("CBR daily decode failed", zap.Error(err),
zap.String("currency", valute.ISOCharCode), zap.String("endpoint", endpoint), zap.String("currency", valute.ISOCharCode), zap.String("endpoint", endpoint),
) )
@@ -293,16 +284,13 @@ func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (s
return computePrice(entry.Value, entry.Nominal) return computePrice(entry.Value, entry.Nominal)
} }
func (c *cbrConnector) fetchHistoricalRate( //nolint:funlen func (c *cbrConnector) fetchHistoricalRate(ctx context.Context, valute valuteInfo, date time.Time) (string, error) {
ctx context.Context, valute valuteInfo, date time.Time,
) (string, error) {
query := map[string]string{ query := map[string]string{
"date_req1": date.Format("02/01/2006"), "date_req1": date.Format("02/01/2006"),
"date_req2": date.Format("02/01/2006"), "date_req2": date.Format("02/01/2006"),
"VAL_NM_RQ": valute.ID, "VAL_NM_RQ": valute.ID,
} }
dateStr := date.Format("2006-01-02") dateStr := date.Format("2006-01-02")
endpoint, err := c.buildURL(c.dynamicPath, query) endpoint, err := c.buildURL(c.dynamicPath, query)
if err != nil { if err != nil {
return "", err return "", err
@@ -377,9 +365,7 @@ func (c *cbrConnector) buildURL(path string, query map[string]string) (string, e
if err != nil { if err != nil {
return "", merrors.InternalWrap(err, "cbr: parse base url") return "", merrors.InternalWrap(err, "cbr: parse base url")
} }
base.Path = strings.TrimRight(base.Path, "/") + path base.Path = strings.TrimRight(base.Path, "/") + path
q := base.Query() q := base.Query()
for key, value := range query { for key, value := range query {
q.Set(key, value) q.Set(key, value)
@@ -415,13 +401,13 @@ type valuteMapping struct {
byID map[string]valuteInfo byID map[string]valuteInfo
} }
func buildValuteMapping(logger mlogger.Logger, items []valuteItem) (*valuteMapping, error) { //nolint:cyclop,gocognit,nestif func buildValuteMapping(logger *zap.Logger, items []valuteItem) (*valuteMapping, error) {
byISO := make(map[string]valuteInfo, len(items)) byISO := make(map[string]valuteInfo, len(items))
byID := make(map[string]valuteInfo, len(items)) byID := make(map[string]valuteInfo, len(items))
byNum := make(map[string]string, len(items)) byNum := make(map[string]string, len(items))
for _, item := range items { for _, item := range items {
valuteID := strings.TrimSpace(item.ID) id := strings.TrimSpace(item.ID)
isoChar := strings.ToUpper(strings.TrimSpace(item.ISOChar)) isoChar := strings.ToUpper(strings.TrimSpace(item.ISOChar))
isoNum := strings.TrimSpace(item.ISONum) isoNum := strings.TrimSpace(item.ISONum)
name := strings.TrimSpace(item.Name) name := strings.TrimSpace(item.Name)
@@ -432,18 +418,17 @@ func buildValuteMapping(logger mlogger.Logger, items []valuteItem) (*valuteMappi
return nil, merrors.InvalidDataType("cbr: parse directory nominal: " + err.Error()) return nil, merrors.InvalidDataType("cbr: parse directory nominal: " + err.Error())
} }
if valuteID == "" || isoChar == "" { if id == "" || isoChar == "" {
logger.Info("Skipping invalid currency entry", logger.Info("Skipping invalid currency entry",
zap.String("id", valuteID), zap.String("id", id),
zap.String("iso_char", isoChar), zap.String("iso_char", isoChar),
zap.String("name", name), zap.String("name", name),
) )
continue continue
} }
info := valuteInfo{ info := valuteInfo{
ID: valuteID, ID: id,
ISOCharCode: isoChar, ISOCharCode: isoChar,
ISONumCode: isoNum, ISONumCode: isoNum,
Name: name, Name: name,
@@ -454,12 +439,11 @@ func buildValuteMapping(logger mlogger.Logger, items []valuteItem) (*valuteMappi
// Handle duplicate ISO char codes (e.g. DEM with different IDs / nominals). // Handle duplicate ISO char codes (e.g. DEM with different IDs / nominals).
if existing, ok := byISO[isoChar]; ok { if existing, ok := byISO[isoChar]; ok {
// Same ISO + same ID: duplicate entry, just ignore. // Same ISO + same ID: duplicate entry, just ignore.
if existing.ID == valuteID { if existing.ID == id {
logger.Debug("Duplicate directory entry for same ISO and ID, ignoring", logger.Debug("Duplicate directory entry for same ISO and ID, ignoring",
zap.String("iso_code", isoChar), zap.String("iso_code", isoChar),
zap.String("id", valuteID), zap.String("id", id),
) )
continue continue
} }
@@ -469,12 +453,11 @@ func buildValuteMapping(logger mlogger.Logger, items []valuteItem) (*valuteMappi
// 2) Otherwise prefer smaller nominal // 2) Otherwise prefer smaller nominal
keepExisting := true keepExisting := true
switch { if existing.Nominal != 1 && info.Nominal == 1 {
case existing.Nominal != 1 && info.Nominal == 1:
keepExisting = false keepExisting = false
case existing.Nominal == 1 && info.Nominal != 1: } else if existing.Nominal == 1 && info.Nominal != 1 {
keepExisting = true keepExisting = true
case info.Nominal < existing.Nominal: } else if info.Nominal < existing.Nominal {
keepExisting = false keepExisting = false
} }
@@ -501,18 +484,17 @@ func buildValuteMapping(logger mlogger.Logger, items []valuteItem) (*valuteMappi
// Update byID: drop old ID, add new one // Update byID: drop old ID, add new one
delete(byID, existing.ID) delete(byID, existing.ID)
byID[valuteID] = info byID[id] = info
// Update ISO mapping // Update ISO mapping
byISO[isoChar] = info byISO[isoChar] = info
// Update numeric-code index if present // Update numeric-code index if present
if isoNum != "" { if isoNum != "" {
if existingID, ok := byNum[isoNum]; ok && existingID != valuteID { if existingID, ok := byNum[isoNum]; ok && existingID != id {
return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum) return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum)
} }
byNum[isoNum] = id
byNum[isoNum] = valuteID
} }
continue continue
@@ -520,24 +502,21 @@ func buildValuteMapping(logger mlogger.Logger, items []valuteItem) (*valuteMappi
// No existing ISO entry, do normal uniqueness checks. // No existing ISO entry, do normal uniqueness checks.
if existing, ok := byID[valuteID]; ok && existing.ISOCharCode != isoChar { if existing, ok := byID[id]; ok && existing.ISOCharCode != isoChar {
return nil, merrors.InvalidDataType("cbr: duplicate valute id " + valuteID) return nil, merrors.InvalidDataType("cbr: duplicate valute id " + id)
} }
if isoNum != "" { if isoNum != "" {
if existingID, ok := byNum[isoNum]; ok && existingID != valuteID { if existingID, ok := byNum[isoNum]; ok && existingID != id {
return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum) return nil, merrors.InvalidDataType("cbr: duplicate ISO numeric code " + isoNum)
} }
byNum[isoNum] = id
byNum[isoNum] = valuteID
} }
logger.Info("Installing currency code", logger.Info("Installing currency code", zap.String("iso_code", isoChar), zap.String("id", id), zap.Int64("nominal", nominal))
zap.String("iso_code", isoChar), zap.String("id", valuteID), zap.Int64("nominal", nominal),
)
byISO[isoChar] = info byISO[isoChar] = info
byID[valuteID] = info byID[id] = info
} }
if len(byISO) == 0 { if len(byISO) == 0 {
@@ -567,7 +546,6 @@ func (d *dailyRates) find(id string) *dailyValute {
if d == nil { if d == nil {
return nil return nil
} }
for idx := range d.Valutes { for idx := range d.Valutes {
if strings.EqualFold(strings.TrimSpace(d.Valutes[idx].ID), id) { if strings.EqualFold(strings.TrimSpace(d.Valutes[idx].ID), id) {
return &d.Valutes[idx] return &d.Valutes[idx]
@@ -591,9 +569,7 @@ func (d *dynamicRates) find(id string, date time.Time) *dynamicRecord {
if d == nil { if d == nil {
return nil return nil
} }
target := date.Format("02.01.2006") target := date.Format("02.01.2006")
for idx := range d.Records { for idx := range d.Records {
rec := &d.Records[idx] rec := &d.Records[idx]
if !strings.EqualFold(strings.TrimSpace(rec.ID), id) { if !strings.EqualFold(strings.TrimSpace(rec.ID), id) {
@@ -687,7 +663,7 @@ func computePrice(value string, nominalStr string) (string, error) {
den := big.NewRat(nominal, 1) den := big.NewRat(nominal, 1)
price := new(big.Rat).Quo(r, den) price := new(big.Rat).Quo(r, den)
return price.FloatString(8), nil //nolint:mnd return price.FloatString(8), nil
} }
func formatSymbol(iso string, asOf *time.Time) string { func formatSymbol(iso string, asOf *time.Time) string {

View File

@@ -1,4 +1,4 @@
package cbr //nolint:testpackage package cbr
import ( import (
"context" "context"
@@ -14,8 +14,6 @@ import (
) )
func TestFetchTickerDaily(t *testing.T) { func TestFetchTickerDaily(t *testing.T) {
t.Parallel()
transport := &stubRoundTripper{ transport := &stubRoundTripper{
responses: map[string]stubResponse{ responses: map[string]stubResponse{
"/scripts/XML_valFull.asp": {body: valuteDirectoryXML}, "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
@@ -49,16 +47,10 @@ func TestFetchTickerDaily(t *testing.T) {
} }
func TestFetchTickerValidatesDailyEntry(t *testing.T) { func TestFetchTickerValidatesDailyEntry(t *testing.T) {
t.Parallel()
transport := &stubRoundTripper{ transport := &stubRoundTripper{
responses: map[string]stubResponse{ responses: map[string]stubResponse{
"/scripts/XML_valFull.asp": {body: valuteDirectoryXML}, "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
"/scripts/XML_daily.asp": { "/scripts/XML_daily.asp": {body: strings.ReplaceAll(dailyRatesXML, "<CharCode>USD</CharCode>", "<CharCode>XXX</CharCode>")},
body: strings.ReplaceAll(
dailyRatesXML, "<CharCode>USD</CharCode>", "<CharCode>XXX</CharCode>",
),
},
}, },
} }
@@ -76,8 +68,6 @@ func TestFetchTickerValidatesDailyEntry(t *testing.T) {
} }
func TestFetchTickerHistorical(t *testing.T) { func TestFetchTickerHistorical(t *testing.T) {
t.Parallel()
transport := &stubRoundTripper{ transport := &stubRoundTripper{
responses: map[string]stubResponse{ responses: map[string]stubResponse{
"/scripts/XML_valFull.asp": {body: valuteDirectoryXML}, "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
@@ -85,13 +75,13 @@ func TestFetchTickerHistorical(t *testing.T) {
body: dynamicRatesXML, body: dynamicRatesXML,
check: func(r *http.Request) error { check: func(r *http.Request) error {
if got := r.URL.Query().Get("VAL_NM_RQ"); got != "R01235" { if got := r.URL.Query().Get("VAL_NM_RQ"); got != "R01235" {
return fmt.Errorf("unexpected valute id: %s", got) //nolint:err113 return fmt.Errorf("unexpected valute id: %s", got)
} }
if got := r.URL.Query().Get("date_req1"); got != "05/01/2023" { if got := r.URL.Query().Get("date_req1"); got != "05/01/2023" {
return fmt.Errorf("unexpected date_req1: %s", got) //nolint:err113 return fmt.Errorf("unexpected date_req1: %s", got)
} }
if got := r.URL.Query().Get("date_req2"); got != "05/01/2023" { if got := r.URL.Query().Get("date_req2"); got != "05/01/2023" {
return fmt.Errorf("unexpected date_req2: %s", got) //nolint:err113 return fmt.Errorf("unexpected date_req2: %s", got)
} }
return nil return nil
}, },
@@ -121,8 +111,6 @@ func TestFetchTickerHistorical(t *testing.T) {
} }
func TestFetchTickerUnknownCurrency(t *testing.T) { func TestFetchTickerUnknownCurrency(t *testing.T) {
t.Parallel()
transport := &stubRoundTripper{ transport := &stubRoundTripper{
responses: map[string]stubResponse{ responses: map[string]stubResponse{
"/scripts/XML_valFull.asp": {body: valuteDirectoryXML}, "/scripts/XML_valFull.asp": {body: valuteDirectoryXML},
@@ -148,8 +136,6 @@ func TestFetchTickerUnknownCurrency(t *testing.T) {
} }
func TestFetchTickerRespectsCustomPaths(t *testing.T) { func TestFetchTickerRespectsCustomPaths(t *testing.T) {
t.Parallel()
transport := &stubRoundTripper{ transport := &stubRoundTripper{
responses: map[string]stubResponse{ responses: map[string]stubResponse{
"/dir.xml": {body: valuteDirectoryXML}, "/dir.xml": {body: valuteDirectoryXML},
@@ -214,11 +200,11 @@ type stubRoundTripper struct {
func (s *stubRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { func (s *stubRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if s.responses == nil { if s.responses == nil {
return nil, fmt.Errorf("no responses configured") //nolint:err113 return nil, fmt.Errorf("no responses configured")
} }
res, ok := s.responses[req.URL.Path] res, ok := s.responses[req.URL.Path]
if !ok { if !ok {
return nil, fmt.Errorf("unexpected request path: %s", req.URL.Path) //nolint:err113 return nil, fmt.Errorf("unexpected request path: %s", req.URL.Path)
} }
if res.check != nil { if res.check != nil {
if err := res.check(req); err != nil { if err := res.check(req); err != nil {

View File

@@ -5,7 +5,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -18,7 +17,7 @@ const (
type httpClient struct { type httpClient struct {
client *http.Client client *http.Client
headers http.Header headers http.Header
logger mlogger.Logger logger *zap.Logger
} }
type httpClientOptions struct { type httpClientOptions struct {
@@ -27,7 +26,7 @@ type httpClientOptions struct {
referer string referer string
} }
func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOptions) *httpClient { func newHTTPClient(logger *zap.Logger, client *http.Client, opts httpClientOptions) *httpClient {
userAgent := opts.userAgent userAgent := opts.userAgent
if strings.TrimSpace(userAgent) == "" { if strings.TrimSpace(userAgent) == "" {
userAgent = defaultUserAgent userAgent = defaultUserAgent
@@ -42,20 +41,20 @@ func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOp
if strings.TrimSpace(referer) == "" { if strings.TrimSpace(referer) == "" {
referer = defaultCBRBaseURL referer = defaultCBRBaseURL
} }
httpLogger := logger.Named("http_client") l := logger.Named("http_client")
headers := make(http.Header, 3) headers := make(http.Header, 3)
headers.Set("User-Agent", userAgent) headers.Set("User-Agent", userAgent)
headers.Set("Accept", accept) headers.Set("Accept", accept)
headers.Set("Referer", referer) headers.Set("Referer", referer)
httpLogger.Info("HTTP client initialized", zap.String("user_agent", userAgent), l.Info("HTTP client initialized", zap.String("user_agent", userAgent),
zap.String("accept", accept), zap.String("referrer", referer)) zap.String("accept", accept), zap.String("referrer", referer))
return &httpClient{ return &httpClient{
client: client, client: client,
headers: headers, headers: headers,
logger: httpLogger, logger: l,
} }
} }
@@ -69,7 +68,6 @@ func (h *httpClient) Do(req *http.Request) (*http.Response, error) {
if enriched.Header.Get(key) != "" { if enriched.Header.Get(key) != "" {
continue continue
} }
for _, value := range values { for _, value := range values {
enriched.Header.Add(key, value) enriched.Header.Add(key, value)
} }

View File

@@ -29,36 +29,29 @@ type coingeckoConnector struct {
const defaultCoinGeckoBaseURL = "https://api.coingecko.com/api/v3" const defaultCoinGeckoBaseURL = "https://api.coingecko.com/api/v3"
const ( const (
defaultDialTimeout = 5 * time.Second defaultDialTimeoutSeconds = 5 * time.Second
defaultDialKeepAlive = 30 * time.Second defaultDialKeepAliveSeconds = 30 * time.Second
defaultTLSHandshakeTimeout = 5 * time.Second defaultTLSHandshakeTimeoutSeconds = 5 * time.Second
defaultResponseHeaderTimeout = 10 * time.Second defaultResponseHeaderTimeoutSeconds = 10 * time.Second
defaultRequestTimeout = 10 * time.Second defaultRequestTimeoutSeconds = 10 * time.Second
) )
const ( func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) {
expectedSymbolParts = 2
tsToMillis = 1000
)
func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { //nolint:ireturn
baseURL := defaultCoinGeckoBaseURL baseURL := defaultCoinGeckoBaseURL
provider := strings.ToLower(mmodel.DriverCoinGecko.String()) provider := strings.ToLower(mmodel.DriverCoinGecko.String())
dialTimeout := defaultDialTimeout dialTimeout := defaultDialTimeoutSeconds
dialKeepAlive := defaultDialKeepAlive dialKeepAlive := defaultDialKeepAliveSeconds
tlsHandshakeTimeout := defaultTLSHandshakeTimeout tlsHandshakeTimeout := defaultTLSHandshakeTimeoutSeconds
responseHeaderTimeout := defaultResponseHeaderTimeout responseHeaderTimeout := defaultResponseHeaderTimeoutSeconds
requestTimeout := defaultRequestTimeout requestTimeout := defaultRequestTimeoutSeconds
if settings != nil { if settings != nil {
if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" { if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" {
baseURL = strings.TrimSpace(value) baseURL = strings.TrimSpace(value)
} }
if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" { if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" {
provider = strings.TrimSpace(value) provider = strings.TrimSpace(value)
} }
dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout) dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout)
dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive) dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive)
tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout) tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout)
@@ -95,7 +88,6 @@ func (c *coingeckoConnector) ID() mmodel.Driver {
return c.id return c.id
} }
//nolint:cyclop,funlen
func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) { func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) {
coinID, vsCurrency, err := parseSymbol(symbol) coinID, vsCurrency, err := parseSymbol(symbol)
if err != nil { if err != nil {
@@ -106,7 +98,6 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m
if err != nil { if err != nil {
return nil, merrors.InternalWrap(err, "coingecko: parse base url") return nil, merrors.InternalWrap(err, "coingecko: parse base url")
} }
endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/simple/price" endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/simple/price"
query := endpoint.Query() query := endpoint.Query()
query.Set("ids", coinID) query.Set("ids", coinID)
@@ -122,51 +113,44 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m
resp, err := c.client.Do(req) resp, err := c.client.Do(req)
if err != nil { if err != nil {
c.logger.Warn("CoinGecko request failed", zap.String("symbol", symbol), zap.Error(err)) c.logger.Warn("CoinGecko request failed", zap.String("symbol", symbol), zap.Error(err))
return nil, merrors.InternalWrap(err, "coingecko: request failed") return nil, merrors.InternalWrap(err, "coingecko: request failed")
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
c.logger.Warn("CoinGecko returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode)) c.logger.Warn("CoinGecko returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))
return nil, merrors.Internal("coingecko: unexpected status " + strconv.Itoa(resp.StatusCode)) return nil, merrors.Internal("coingecko: unexpected status " + strconv.Itoa(resp.StatusCode))
} }
decoder := json.NewDecoder(resp.Body) decoder := json.NewDecoder(resp.Body)
decoder.UseNumber() decoder.UseNumber()
var payload map[string]map[string]any var payload map[string]map[string]interface{}
if err := decoder.Decode(&payload); err != nil {
decodeErr := decoder.Decode(&payload) c.logger.Warn("CoinGecko decode failed", zap.String("symbol", symbol), zap.Error(err))
if decodeErr != nil { return nil, merrors.InternalWrap(err, "coingecko: decode response")
c.logger.Warn("CoinGecko decode failed", zap.String("symbol", symbol), zap.Error(decodeErr))
return nil, merrors.InternalWrap(decodeErr, "coingecko: decode response")
} }
coinData, coinFound := payload[coinID] coinData, ok := payload[coinID]
if !coinFound { if !ok {
return nil, merrors.Internal("coingecko: coin id not found in response") return nil, merrors.Internal("coingecko: coin id not found in response")
} }
priceValue, ok := coinData[vsCurrency]
priceValue, priceFound := coinData[vsCurrency] if !ok {
if !priceFound {
return nil, merrors.Internal("coingecko: vs currency not found in response") return nil, merrors.Internal("coingecko: vs currency not found in response")
} }
price, priceOk := toFloat(priceValue) price, ok := toFloat(priceValue)
if !priceOk || price <= 0 { if !ok || price <= 0 {
return nil, merrors.Internal("coingecko: invalid price value in response") return nil, merrors.Internal("coingecko: invalid price value in response")
} }
priceStr := strconv.FormatFloat(price, 'f', -1, 64) priceStr := strconv.FormatFloat(price, 'f', -1, 64)
timestamp := time.Now().UnixMilli() timestamp := time.Now().UnixMilli()
if tsValue, ok := coinData["last_updated_at"]; ok {
if tsValue, tsFound := coinData["last_updated_at"]; tsFound { if tsFloat, ok := toFloat(tsValue); ok && tsFloat > 0 {
if tsFloat, tsOk := toFloat(tsValue); tsOk && tsFloat > 0 { tsMillis := int64(tsFloat * 1000)
tsMillis := int64(tsFloat * tsToMillis)
if tsMillis > 0 { if tsMillis > 0 {
timestamp = tsMillis timestamp = tsMillis
} }
@@ -195,16 +179,14 @@ func parseSymbol(symbol string) (string, string, error) {
case ':', '/', '-', '_': case ':', '/', '-', '_':
return true return true
} }
return false return false
}) })
if len(parts) != expectedSymbolParts { if len(parts) != 2 {
return "", "", merrors.InvalidArgument("coingecko: symbol must be <coin_id>/<vs_currency>", "symbol") return "", "", merrors.InvalidArgument("coingecko: symbol must be <coin_id>/<vs_currency>", "symbol")
} }
coinID := strings.TrimSpace(parts[0]) coinID := strings.TrimSpace(parts[0])
vsCurrency := strings.TrimSpace(parts[1]) vsCurrency := strings.TrimSpace(parts[1])
if coinID == "" || vsCurrency == "" { if coinID == "" || vsCurrency == "" {
return "", "", merrors.InvalidArgument("coingecko: symbol contains empty segments", "symbol") return "", "", merrors.InvalidArgument("coingecko: symbol contains empty segments", "symbol")
@@ -213,31 +195,28 @@ func parseSymbol(symbol string) (string, string, error) {
return coinID, vsCurrency, nil return coinID, vsCurrency, nil
} }
func toFloat(value any) (float64, bool) { func toFloat(value interface{}) (float64, bool) {
switch val := value.(type) { switch v := value.(type) {
case json.Number: case json.Number:
f, err := val.Float64() f, err := v.Float64()
if err != nil { if err != nil {
return 0, false return 0, false
} }
return f, true return f, true
case float64: case float64:
return val, true return v, true
case float32: case float32:
return float64(val), true return float64(v), true
case int: case int:
return float64(val), true return float64(v), true
case int64: case int64:
return float64(val), true return float64(v), true
case uint64: case uint64:
return float64(val), true return float64(v), true
case string: case string:
parsed, parseErr := strconv.ParseFloat(val, 64) if parsed, err := strconv.ParseFloat(v, 64); err == nil {
if parseErr == nil {
return parsed, true return parsed, true
} }
} }
return 0, false return 0, false
} }

View File

@@ -1,4 +1,4 @@
package common //nolint:revive // package provides shared market connector utilities package common
import ( import (
"strconv" "strconv"
@@ -8,46 +8,39 @@ import (
) )
// DurationSetting reads a positive duration override from settings or returns def when the value is missing or invalid. // DurationSetting reads a positive duration override from settings or returns def when the value is missing or invalid.
//
//nolint:cyclop
func DurationSetting(settings model.SettingsT, key string, def time.Duration) time.Duration { func DurationSetting(settings model.SettingsT, key string, def time.Duration) time.Duration {
if settings == nil { if settings == nil {
return def return def
} }
value, ok := settings[key] value, ok := settings[key]
if !ok { if !ok {
return def return def
} }
switch val := value.(type) { switch v := value.(type) {
case time.Duration: case time.Duration:
if val > 0 { if v > 0 {
return val return v
} }
case int: case int:
if val > 0 { if v > 0 {
return time.Duration(val) * time.Second return time.Duration(v) * time.Second
} }
case int64: case int64:
if val > 0 { if v > 0 {
return time.Duration(val) * time.Second return time.Duration(v) * time.Second
} }
case float64: case float64:
if val > 0 { if v > 0 {
return time.Duration(val * float64(time.Second)) return time.Duration(v * float64(time.Second))
} }
case string: case string:
parsed, parseErr := time.ParseDuration(val) if parsed, err := time.ParseDuration(v); err == nil && parsed > 0 {
if parseErr == nil && parsed > 0 {
return parsed return parsed
} }
if seconds, err := strconv.ParseFloat(v, 64); err == nil && seconds > 0 {
seconds, floatErr := strconv.ParseFloat(val, 64)
if floatErr == nil && seconds > 0 {
return time.Duration(seconds * float64(time.Second)) return time.Duration(seconds * float64(time.Second))
} }
} }
return def return def
} }

View File

@@ -24,19 +24,16 @@ const (
) )
type Server interface { type Server interface {
SetStatus(status health.ServiceStatus) SetStatus(health.ServiceStatus)
Close(ctx context.Context) Close(context.Context)
} }
//nolint:ireturn
func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error) { func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error) {
if logger == nil { if logger == nil {
return nil, merrors.InvalidArgument("metrics: logger is nil") return nil, merrors.InvalidArgument("metrics: logger is nil")
} }
if cfg == nil || !cfg.Enabled { if cfg == nil || !cfg.Enabled {
logger.Debug("Metrics disabled; using noop server") logger.Debug("Metrics disabled; using noop server")
return noopServer{}, nil return noopServer{}, nil
} }
@@ -50,9 +47,7 @@ func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error)
router.Handle("/metrics", promhttp.Handler()) router.Handle("/metrics", promhttp.Handler())
var healthRouter routers.Health var healthRouter routers.Health
if hr, err := routers.NewHealthRouter(metricsLogger, router, ""); err != nil {
hr, err := routers.NewHealthRouter(metricsLogger, router, "")
if err != nil {
metricsLogger.Warn("Failed to initialise health router", zap.Error(err)) metricsLogger.Warn("Failed to initialise health router", zap.Error(err))
} else { } else {
hr.SetStatus(health.SSStarting) hr.SetStatus(health.SSStarting)
@@ -65,7 +60,7 @@ func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error)
ReadHeaderTimeout: readHeaderTimeout, ReadHeaderTimeout: readHeaderTimeout,
} }
wrapper := &httpServerWrapper{ ms := &httpServerWrapper{
logger: metricsLogger, logger: metricsLogger,
server: httpServer, server: httpServer,
health: healthRouter, health: healthRouter,
@@ -74,9 +69,7 @@ func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error)
go func() { go func() {
metricsLogger.Info("Prometheus endpoint listening", zap.String("address", address)) metricsLogger.Info("Prometheus endpoint listening", zap.String("address", address))
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
err := httpServer.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
metricsLogger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err)) metricsLogger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err))
if healthRouter != nil { if healthRouter != nil {
healthRouter.SetStatus(health.SSTerminating) healthRouter.SetStatus(health.SSTerminating)
@@ -84,7 +77,7 @@ func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error)
} }
}() }()
return wrapper, nil return ms, nil
} }
type httpServerWrapper struct { type httpServerWrapper struct {
@@ -98,7 +91,6 @@ func (s *httpServerWrapper) SetStatus(status health.ServiceStatus) {
if s == nil || s.health == nil { if s == nil || s.health == nil {
return return
} }
s.logger.Debug("Updating metrics health status", zap.String("status", string(status))) s.logger.Debug("Updating metrics health status", zap.String("status", string(status)))
s.health.SetStatus(status) s.health.SetStatus(status)
} }
@@ -118,12 +110,10 @@ func (s *httpServerWrapper) Close(ctx context.Context) {
return return
} }
//nolint:contextcheck
shutdownCtx := ctx shutdownCtx := ctx
if shutdownCtx == nil { if shutdownCtx == nil {
shutdownCtx = context.Background() shutdownCtx = context.Background()
} }
if s.timeout > 0 { if s.timeout > 0 {
var cancel context.CancelFunc var cancel context.CancelFunc
shutdownCtx, cancel = context.WithTimeout(shutdownCtx, s.timeout) shutdownCtx, cancel = context.WithTimeout(shutdownCtx, s.timeout)
@@ -139,6 +129,6 @@ func (s *httpServerWrapper) Close(ctx context.Context) {
type noopServer struct{} type noopServer struct{}
func (noopServer) SetStatus(_ health.ServiceStatus) {} func (noopServer) SetStatus(health.ServiceStatus) {}
func (noopServer) Close(_ context.Context) {} func (noopServer) Close(context.Context) {}

View File

@@ -26,18 +26,16 @@ func main() {
flag.Parse() flag.Parse()
logger := lf.NewLogger(*debugFlag).Named("fx_ingestor") logger := lf.NewLogger(*debugFlag).Named("fx_ingestor")
logger = logger.With(zap.String("instance_id", discovery.InstanceID())) logger = logger.With(zap.String("instance_id", discovery.InstanceID()))
defer logger.Sync() //nolint:errcheck defer logger.Sync()
appVersion := appversion.Create() av := appversion.Create()
if *versionFlag { if *versionFlag {
fmt.Fprintln(os.Stdout, appVersion.Print()) fmt.Fprintln(os.Stdout, av.Print())
return return
} }
logger.Info("Starting "+appVersion.Program(), zap.String("version", appVersion.Info())) logger.Info(fmt.Sprintf("Starting %s", av.Program()), zap.String("version", av.Info()))
ctx, cancel := signalctx.WithSignals(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, cancel := signalctx.WithSignals(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel() defer cancel()
@@ -49,10 +47,8 @@ func main() {
if err := application.Run(ctx); err != nil { if err := application.Run(ctx); err != nil {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
logger.Info("FX ingestor stopped") logger.Info("FX ingestor stopped")
return return
} }
logger.Error("Ingestor terminated with error", zap.Error(err)) logger.Error("Ingestor terminated with error", zap.Error(err))
} }
} }

View File

@@ -68,7 +68,6 @@ type Quote struct {
BaseAmount *moneyv1.Money BaseAmount *moneyv1.Money
QuoteAmount *moneyv1.Money QuoteAmount *moneyv1.Money
ExpiresAt time.Time ExpiresAt time.Time
PricedAt time.Time
Provider string Provider string
RateRef string RateRef string
Firm bool Firm bool
@@ -238,10 +237,6 @@ func fromProtoQuote(quote *oraclev1.Quote) *Quote {
if quote == nil { if quote == nil {
return nil return nil
} }
pricedAt := time.Time{}
if ts := quote.GetPricedAt(); ts != nil {
pricedAt = ts.AsTime()
}
return &Quote{ return &Quote{
QuoteRef: quote.GetQuoteRef(), QuoteRef: quote.GetQuoteRef(),
Pair: quote.Pair, Pair: quote.Pair,
@@ -250,7 +245,6 @@ func fromProtoQuote(quote *oraclev1.Quote) *Quote {
BaseAmount: quote.BaseAmount, BaseAmount: quote.BaseAmount,
QuoteAmount: quote.QuoteAmount, QuoteAmount: quote.QuoteAmount,
ExpiresAt: time.UnixMilli(quote.GetExpiresAtUnixMs()), ExpiresAt: time.UnixMilli(quote.GetExpiresAtUnixMs()),
PricedAt: pricedAt,
Provider: quote.GetProvider(), Provider: quote.GetProvider(),
RateRef: quote.GetRateRef(), RateRef: quote.GetRateRef(),
Firm: quote.GetFirm(), Firm: quote.GetFirm(),

View File

@@ -9,7 +9,6 @@ import (
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
) )
type stubOracle struct { type stubOracle struct {
@@ -76,7 +75,6 @@ func TestLatestRate(t *testing.T) {
func TestGetQuote(t *testing.T) { func TestGetQuote(t *testing.T) {
expiresAt := time.Date(2024, 2, 2, 12, 0, 0, 0, time.UTC) expiresAt := time.Date(2024, 2, 2, 12, 0, 0, 0, time.UTC)
pricedAt := time.Date(2024, 2, 2, 11, 59, 0, 0, time.UTC)
stub := &stubOracle{ stub := &stubOracle{
quoteResp: &oraclev1.GetQuoteResponse{ quoteResp: &oraclev1.GetQuoteResponse{
Quote: &oraclev1.Quote{ Quote: &oraclev1.Quote{
@@ -87,7 +85,6 @@ func TestGetQuote(t *testing.T) {
BaseAmount: &moneyv1.Money{Amount: "100.00", Currency: "GBP"}, BaseAmount: &moneyv1.Money{Amount: "100.00", Currency: "GBP"},
QuoteAmount: &moneyv1.Money{Amount: "125.00", Currency: "USD"}, QuoteAmount: &moneyv1.Money{Amount: "125.00", Currency: "USD"},
ExpiresAtUnixMs: expiresAt.UnixMilli(), ExpiresAtUnixMs: expiresAt.UnixMilli(),
PricedAt: timestamppb.New(pricedAt),
Provider: "Test", Provider: "Test",
RateRef: "test-ref", RateRef: "test-ref",
Firm: true, Firm: true,
@@ -116,7 +113,4 @@ func TestGetQuote(t *testing.T) {
if resp.QuoteRef != "quote-123" || resp.Price != "1.2500" || !resp.ExpiresAt.Equal(expiresAt) { if resp.QuoteRef != "quote-123" || resp.Price != "1.2500" || !resp.ExpiresAt.Equal(expiresAt) {
t.Fatalf("unexpected quote response: %+v", resp) t.Fatalf("unexpected quote response: %+v", resp)
} }
if !resp.PricedAt.Equal(pricedAt) {
t.Fatalf("expected priced_at %s, got %s", pricedAt, resp.PricedAt)
}
} }

View File

@@ -11,8 +11,6 @@ grpc:
metrics: metrics:
address: ":9400" address: ":9400"
max_quote_ttl_ms: 600000
database: database:
driver: mongodb driver: mongodb
settings: settings:

View File

@@ -11,8 +11,6 @@ grpc:
metrics: metrics:
address: ":9400" address: ":9400"
max_quote_ttl_ms: 600000
database: database:
driver: mongodb driver: mongodb
settings: settings:

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/fx/oracle module github.com/tech/sendico/fx/oracle
go 1.25.7 go 1.25.6
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
@@ -13,7 +13,7 @@ require (
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -25,28 +25,28 @@ require (
github.com/casbin/govaluate v1.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-chi/chi/v5 v5.2.4 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.49.0 // indirect github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect
) )

View File

@@ -38,8 +38,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -152,16 +152,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/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 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -191,16 +191,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,10 +208,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -22,28 +22,11 @@ type Imp struct {
file string file string
debug bool debug bool
config *config config *grpcapp.Config
app *grpcapp.App[storage.Repository] app *grpcapp.App[storage.Repository]
service *oracle.Service service *oracle.Service
} }
type config struct {
*grpcapp.Config `yaml:",inline"`
MaxQuoteTTLMs int64 `yaml:"max_quote_ttl_ms"`
}
const (
defaultMaxQuoteTTL = 10 * time.Minute
defaultMaxQuoteTTLMillis = int64(defaultMaxQuoteTTL / time.Millisecond)
)
func (c *config) maxQuoteTTLMillis() int64 {
if c == nil || c.MaxQuoteTTLMs <= 0 {
return defaultMaxQuoteTTLMillis
}
return c.MaxQuoteTTLMs
}
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
return &Imp{ return &Imp{
logger: logger.Named("server"), logger: logger.Named("server"),
@@ -80,18 +63,12 @@ func (i *Imp) Start() error {
} }
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) {
svc := oracle.NewService( svc := oracle.NewService(logger, repo, producer, cfg.GRPC.DiscoveryInvokeURI())
logger,
repo,
producer,
cfg.GRPC.DiscoveryInvokeURI(),
oracle.WithMaxQuoteTTLMillis(cfg.maxQuoteTTLMillis()),
)
i.service = svc i.service = svc
return svc, nil return svc, nil
} }
app, err := grpcapp.NewApp(i.logger, "fx", cfg.Config, i.debug, repoFactory, serviceFactory) app, err := grpcapp.NewApp(i.logger, "fx", cfg, i.debug, repoFactory, serviceFactory)
if err != nil { if err != nil {
return err return err
} }
@@ -100,14 +77,14 @@ func (i *Imp) Start() error {
return i.app.Start() return i.app.Start()
} }
func (i *Imp) loadConfig() (*config, error) { func (i *Imp) loadConfig() (*grpcapp.Config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err return nil, err
} }
cfg := &config{Config: &grpcapp.Config{}} cfg := &grpcapp.Config{}
if err := yaml.Unmarshal(data, cfg); err != nil { if err := yaml.Unmarshal(data, cfg); err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err return nil, err

View File

@@ -130,10 +130,6 @@ func (qc *quoteComputation) buildModelQuote(firm bool, expiryMillis int64, req *
if qc.baseRounded == nil || qc.quoteRounded == nil || qc.priceRounded == nil { if qc.baseRounded == nil || qc.quoteRounded == nil || qc.priceRounded == nil {
return nil, merrors.Internal("oracle: computation not executed") return nil, merrors.Internal("oracle: computation not executed")
} }
pricedAtUnixMs := qc.rate.AsOfUnixMs
if pricedAtUnixMs <= 0 {
pricedAtUnixMs = time.Now().UnixMilli()
}
quote := &model.Quote{ quote := &model.Quote{
QuoteRef: uuid.NewString(), QuoteRef: uuid.NewString(),
@@ -151,7 +147,6 @@ func (qc *quoteComputation) buildModelQuote(firm bool, expiryMillis int64, req *
Amount: formatRat(qc.quoteRounded, qc.quoteScale), Amount: formatRat(qc.quoteRounded, qc.quoteScale),
}, },
AmountType: qc.amountType, AmountType: qc.amountType,
PricedAtUnixMs: pricedAtUnixMs,
RateRef: qc.rate.RateRef, RateRef: qc.rate.RateRef,
Provider: qc.provider, Provider: qc.provider,
PreferredProvider: req.GetPreferredProvider(), PreferredProvider: req.GetPreferredProvider(),

View File

@@ -28,14 +28,6 @@ func (e serviceError) Error() string {
return string(e) return string(e)
} }
const (
defaultMaxQuoteTTL = 10 * time.Minute
defaultMaxQuoteTTLMillis = int64(defaultMaxQuoteTTL / time.Millisecond)
)
// Option configures oracle service behavior.
type Option func(*Service)
var ( var (
errSideRequired = serviceError("oracle: side is required") errSideRequired = serviceError("oracle: side is required")
errAmountsMutuallyExclusive = serviceError("oracle: exactly one amount must be provided") errAmountsMutuallyExclusive = serviceError("oracle: exactly one amount must be provided")
@@ -46,40 +38,21 @@ var (
) )
type Service struct { type Service struct {
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
producer pmessaging.Producer producer pmessaging.Producer
announcer *discovery.Announcer announcer *discovery.Announcer
invokeURI string invokeURI string
maxQuoteTTLMillis int64
oraclev1.UnimplementedOracleServer oraclev1.UnimplementedOracleServer
} }
// WithMaxQuoteTTLMillis caps firm quote TTL requests to the supplied number of milliseconds. func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, invokeURI string) *Service {
func WithMaxQuoteTTLMillis(value int64) Option {
return func(s *Service) {
if value > 0 {
s.maxQuoteTTLMillis = value
}
}
}
func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, invokeURI string, opts ...Option) *Service {
initMetrics() initMetrics()
svc := &Service{ svc := &Service{
logger: logger.Named("oracle"), logger: logger.Named("oracle"),
storage: repo, storage: repo,
producer: prod, producer: prod,
invokeURI: strings.TrimSpace(invokeURI), invokeURI: strings.TrimSpace(invokeURI),
maxQuoteTTLMillis: defaultMaxQuoteTTLMillis,
}
for _, opt := range opts {
if opt != nil {
opt(svc)
}
}
if svc.maxQuoteTTLMillis <= 0 {
svc.maxQuoteTTLMillis = defaultMaxQuoteTTLMillis
} }
svc.startDiscoveryAnnouncer() svc.startDiscoveryAnnouncer()
return svc return svc
@@ -106,7 +79,7 @@ func (s *Service) startDiscoveryAnnouncer() {
} }
announce := discovery.Announcement{ announce := discovery.Announcement{
Service: "FX_ORACLE", Service: "FX_ORACLE",
Operations: []string{discovery.OperationFXQuote}, Operations: []string{"fx.quote"},
InvokeURI: s.invokeURI, InvokeURI: s.invokeURI,
Version: appversion.Create().Short(), Version: appversion.Create().Short(),
} }
@@ -249,16 +222,7 @@ func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteR
expiresAt := int64(0) expiresAt := int64(0)
if req.GetFirm() { if req.GetFirm() {
ttlMs := req.GetTtlMs() expiry, err := computeExpiry(now, req.GetTtlMs())
if ttlMs > s.maxQuoteTTLMillis {
logger.Info(
"Clamping requested firm quote ttl to configured maximum",
zap.Int64("requested_ttl_ms", ttlMs),
zap.Int64("max_ttl_ms", s.maxQuoteTTLMillis),
)
ttlMs = s.maxQuoteTTLMillis
}
expiry, err := computeExpiry(now, ttlMs)
if err != nil { if err != nil {
return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err)
} }

View File

@@ -111,7 +111,6 @@ func (currencyStoreStub) List(ctx context.Context, codes ...string) ([]*model.Cu
func (currencyStoreStub) Upsert(ctx context.Context, currency *model.Currency) error { return nil } func (currencyStoreStub) Upsert(ctx context.Context, currency *model.Currency) error { return nil }
func TestServiceGetQuoteFirm(t *testing.T) { func TestServiceGetQuoteFirm(t *testing.T) {
pricedAt := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
repo := &repositoryStub{} repo := &repositoryStub{}
repo.pairs = &pairStoreStub{ repo.pairs = &pairStoreStub{
getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
@@ -130,7 +129,7 @@ func TestServiceGetQuoteFirm(t *testing.T) {
Ask: "1.10", Ask: "1.10",
Bid: "1.08", Bid: "1.08",
RateRef: "rate#1", RateRef: "rate#1",
AsOfUnixMs: pricedAt.UnixMilli(), AsOfUnixMs: time.Now().UnixMilli(),
}, nil }, nil
}, },
} }
@@ -170,71 +169,9 @@ func TestServiceGetQuoteFirm(t *testing.T) {
if resp.GetQuote().GetQuoteAmount().GetAmount() != "110.00" { if resp.GetQuote().GetQuoteAmount().GetAmount() != "110.00" {
t.Fatalf("unexpected quote amount: %s", resp.GetQuote().GetQuoteAmount().GetAmount()) t.Fatalf("unexpected quote amount: %s", resp.GetQuote().GetQuoteAmount().GetAmount())
} }
if got := resp.GetQuote().GetPricedAt(); got == nil || !got.AsTime().Equal(pricedAt) {
t.Fatalf("expected priced_at %s, got %v", pricedAt, got)
}
if savedQuote.QuoteRef == "" { if savedQuote.QuoteRef == "" {
t.Fatalf("expected quote persisted") t.Fatalf("expected quote persisted")
} }
if savedQuote.PricedAtUnixMs != pricedAt.UnixMilli() {
t.Fatalf("expected stored pricedAtUnixMs %d, got %d", pricedAt.UnixMilli(), savedQuote.PricedAtUnixMs)
}
}
func TestServiceGetQuoteFirm_ClampsTTLToConfiguredMax(t *testing.T) {
const (
configuredMaxTTL = 1 * time.Second
requestedTTL = 1 * time.Minute
)
repo := &repositoryStub{}
repo.pairs = &pairStoreStub{
getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
return &model.Pair{
Pair: pair,
BaseMeta: model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven},
QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven},
}, nil
},
}
repo.rates = &ratesStoreStub{
latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
return &model.RateSnapshot{
Pair: pair,
Provider: provider,
Ask: "1.10",
Bid: "1.08",
RateRef: "rate#1",
AsOfUnixMs: time.Now().UnixMilli(),
}, nil
},
}
repo.quotes = &quotesStoreStub{}
repo.currencies = currencyStoreStub{}
svc := NewService(zap.NewNop(), repo, nil, "", WithMaxQuoteTTLMillis(int64(configuredMaxTTL/time.Millisecond)))
start := time.Now()
resp, err := svc.GetQuote(context.Background(), &oraclev1.GetQuoteRequest{
Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"},
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{
Currency: "USD",
Amount: "100",
}},
Firm: true,
TtlMs: int64(requestedTTL / time.Millisecond),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expiry := time.UnixMilli(resp.GetQuote().GetExpiresAtUnixMs())
if expiry.Before(start) {
t.Fatalf("expected expiry after request start, got %s", expiry)
}
if expiry.After(start.Add(5 * time.Second)) {
t.Fatalf("expected clamped expiry close to 1s max ttl, got %s", expiry)
}
} }
func TestServiceGetQuoteRateNotFound(t *testing.T) { func TestServiceGetQuoteRateNotFound(t *testing.T) {

View File

@@ -2,14 +2,12 @@ package oracle
import ( import (
"strings" "strings"
"time"
"github.com/tech/sendico/fx/storage/model" "github.com/tech/sendico/fx/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types" paymenttypes "github.com/tech/sendico/pkg/payments/types"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
"google.golang.org/protobuf/types/known/timestamppb"
) )
func buildResponseMeta(meta *oraclev1.RequestMeta) *oraclev1.ResponseMeta { func buildResponseMeta(meta *oraclev1.RequestMeta) *oraclev1.ResponseMeta {
@@ -38,7 +36,6 @@ func quoteModelToProto(q *model.Quote) *oraclev1.Quote {
BaseAmount: moneyModelToProto(&q.BaseAmount), BaseAmount: moneyModelToProto(&q.BaseAmount),
QuoteAmount: moneyModelToProto(&q.QuoteAmount), QuoteAmount: moneyModelToProto(&q.QuoteAmount),
ExpiresAtUnixMs: q.ExpiresAtUnixMs, ExpiresAtUnixMs: q.ExpiresAtUnixMs,
PricedAt: timestampFromUnixMillis(q.PricedAtUnixMs, q.CreatedAt),
Provider: q.Provider, Provider: q.Provider,
RateRef: q.RateRef, RateRef: q.RateRef,
Firm: q.Firm, Firm: q.Firm,
@@ -120,13 +117,3 @@ func decimalStringToProto(value string) *moneyv1.Decimal {
} }
return &moneyv1.Decimal{Value: value} return &moneyv1.Decimal{Value: value}
} }
func timestampFromUnixMillis(ms int64, fallback time.Time) *timestamppb.Timestamp {
if ms > 0 {
return timestamppb.New(time.UnixMilli(ms).UTC())
}
if !fallback.IsZero() {
return timestamppb.New(fallback.UTC())
}
return nil
}

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/fx/storage module github.com/tech/sendico/fx/storage
go 1.25.7 go 1.25.6
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
@@ -16,15 +16,15 @@ require (
github.com/casbin/govaluate v1.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.33.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -16,8 +16,6 @@ github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0l
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE= github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
@@ -51,8 +49,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -122,12 +120,12 @@ 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/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 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -136,8 +134,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -152,16 +150,16 @@ 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-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-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.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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View File

@@ -21,7 +21,6 @@ type Quote struct {
QuoteAmount paymenttypes.Money `bson:"quoteAmount" json:"quoteAmount"` QuoteAmount paymenttypes.Money `bson:"quoteAmount" json:"quoteAmount"`
AmountType QuoteAmountType `bson:"amountType" json:"amountType"` AmountType QuoteAmountType `bson:"amountType" json:"amountType"`
ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"` ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"`
PricedAtUnixMs int64 `bson:"pricedAtUnixMs,omitempty" json:"pricedAtUnixMs,omitempty"`
ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"` ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"`
RateRef string `bson:"rateRef" json:"rateRef"` RateRef string `bson:"rateRef" json:"rateRef"`
Provider string `bson:"provider" json:"provider"` Provider string `bson:"provider" json:"provider"`

Some files were not shown because too many files have changed in this diff Show More