Payout Page + Wallet Card Refactor #695

Merged
tech merged 11 commits from SEND066 into main 2026-03-11 16:45:32 +00:00
385 changed files with 3332 additions and 2404 deletions
Showing only changes of commit 3b23eada33 - Show all commits

View File

@@ -1,2 +1,18 @@
ci/dev/mongo.key*
# VCS / editor files
.git
.vscode
.DS_Store
**/.DS_Store
# Local caches and temporary artifacts
.cache
.gocache
**/.gocache
**/tmp/
**/tmp/**
# Frontend local build artifacts (rebuilt in Docker)
frontend/**/.dart_tool
frontend/**/build

2
.gitignore vendored
View File

@@ -10,7 +10,9 @@ generate_protos.sh
update_dep.sh
test.sh
.vscode/
.gocache/
.golangci-cache/
.cache/
.claude/

View File

@@ -47,6 +47,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh bff
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -75,7 +85,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/bff/build-image.sh

View File

@@ -42,6 +42,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh billing_documents
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -70,7 +80,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/billing_documents/build-image.sh

View File

@@ -42,6 +42,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh billing_fees
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -70,7 +80,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/billing_fees/build-image.sh

View File

@@ -43,6 +43,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh callbacks
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -71,7 +81,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/callbacks/build-image.sh

View File

@@ -41,6 +41,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh discovery
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -69,7 +79,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/discovery/build-image.sh

View File

@@ -47,6 +47,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh fx_ingestor
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -75,7 +85,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/fx/build-image.sh

View File

@@ -48,6 +48,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh fx_oracle
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -76,7 +86,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/fx/build-image.sh

View File

@@ -46,6 +46,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_chain
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -74,7 +84,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/chain_gateway/build-image.sh

View File

@@ -45,6 +45,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_mntx
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -73,7 +83,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/mntx/build-image.sh

View File

@@ -43,6 +43,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_tgsettle
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -71,7 +81,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/tgsettle/build-image.sh

View File

@@ -46,6 +46,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_tron
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -74,7 +84,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/tron_gateway/build-image.sh

View File

@@ -42,6 +42,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh ledger
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -70,7 +80,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/ledger/build-image.sh

View File

@@ -45,6 +45,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh notification
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -73,7 +83,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/notification/build-image.sh

View File

@@ -43,6 +43,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh payments_methods
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -71,7 +81,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/payments_methods/build-image.sh

View File

@@ -43,6 +43,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh payments_orchestrator
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -71,7 +81,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/payments_orchestrator/build-image.sh

View File

@@ -43,6 +43,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh payments_quotation
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
@@ -71,7 +81,7 @@ steps:
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
depends_on: [ backend-lint, backend-tests, secrets ]
commands:
- sh ci/scripts/payments_quotation/build-image.sh

View File

@@ -1,10 +1,12 @@
# Sendico Development Environment - Makefile
# Docker Compose + Makefile build system
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend backend-up backend-down backend-rebuild
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend lint-api backend-up backend-down backend-rebuild
COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev
SERVICE ?=
API_GOCACHE ?= $(CURDIR)/.gocache
API_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache
BACKEND_SERVICES := \
dev-discovery \
dev-fx-oracle \
@@ -20,7 +22,7 @@ BACKEND_SERVICES := \
dev-tron-gateway-vault-agent \
dev-tron-gateway \
dev-aurora-gateway \
dev-tgsettle-gateway \
dev-chsettle-gateway \
dev-notification \
dev-callbacks-vault-agent \
dev-callbacks \
@@ -61,7 +63,7 @@ help:
@echo " make build-core Build core services (discovery, ledger, fees, documents)"
@echo " make build-fx Build FX services (oracle, ingestor)"
@echo " make build-payments Build payment orchestrator"
@echo " make build-gateways Build gateway services (chain, tron, aurora, tgsettle)"
@echo " make build-gateways Build gateway services (chain, tron, aurora, chsettle)"
@echo " make build-api Build API services (notification, callbacks, bff)"
@echo " make build-frontend Build Flutter web frontend"
@echo ""
@@ -76,6 +78,7 @@ help:
@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 lint-api Run golangci-lint across all API Go modules"
@echo " make health Check service health"
@echo ""
@echo "Examples:"
@@ -247,7 +250,7 @@ services-up:
dev-chain-gateway \
dev-tron-gateway \
dev-aurora-gateway \
dev-tgsettle-gateway \
dev-chsettle-gateway \
dev-notification \
dev-callbacks \
dev-bff \
@@ -292,7 +295,7 @@ list-services:
@echo " - dev-chain-gateway :50070, :9404 (EVM Blockchain Gateway)"
@echo " - dev-tron-gateway :50071, :9408 (TRON Blockchain Gateway)"
@echo " - dev-aurora-gateway :50075, :9405, :8084 (Card Payouts Simulator)"
@echo " - dev-tgsettle-gateway :50080, :9406 (Telegram Settlements)"
@echo " - dev-chsettle-gateway :50080, :9406 (Chimera Settlements Simulator)"
@echo " - dev-notification :8081 (Notifications)"
@echo " - dev-callbacks :9420 (Webhook Callbacks)"
@echo " - dev-bff :8080 (Backend for Frontend)"
@@ -322,7 +325,7 @@ build-payments:
build-gateways:
@echo "$(GREEN)Building gateway services...$(NC)"
@$(COMPOSE) build dev-chain-gateway dev-tron-gateway dev-aurora-gateway dev-tgsettle-gateway
@$(COMPOSE) build dev-chain-gateway dev-tron-gateway dev-aurora-gateway dev-chsettle-gateway
build-api:
@echo "$(GREEN)Building API services...$(NC)"
@@ -374,3 +377,18 @@ test-frontend:
@cd frontend/pshared && flutter test
@cd frontend/pweb && flutter test
@echo "$(GREEN)✅ All frontend tests passed$(NC)"
# Run Go API linting
lint-api:
@echo "$(GREEN)Running API linting...$(NC)"
@mkdir -p "$(API_GOCACHE)" "$(API_GOLANGCI_LINT_CACHE)"
@failed=""; \
for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Linting $$dir..."; \
(cd "$$dir" && GOCACHE="$(API_GOCACHE)" GOLANGCI_LINT_CACHE="$(API_GOLANGCI_LINT_CACHE)" golangci-lint run --allow-serial-runners --allow-parallel-runners ./...) || failed="$$failed $$dir"; \
done; \
if [ -n "$$failed" ]; then \
echo "$(YELLOW)Lint failed:$$failed$(NC)"; \
exit 1; \
fi
@echo "$(GREEN)✅ All API lint checks passed$(NC)"

View File

@@ -25,14 +25,15 @@ Financial services platform providing payment orchestration, ledger accounting,
| Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway |
| Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway |
| Gateway Aurora | `api/gateway/aurora/` | Card payouts simulator |
| Gateway ChimeraSettle | `api/gateway/chsettle/` | Dummy settlement simulator (fast/slow/success/fail/stuck) |
| Gateway MNTX | `api/gateway/mntx/` | Card payouts |
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
| Gateway TGSettle (legacy) | `api/gateway/tgsettle/` | Legacy Telegram settlement gateway (not used in dev compose) |
| Notification | `api/notification/` | Notifications |
| BFF | `api/edge/bff/` | Backend for frontend |
| Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery |
| Frontend | `frontend/pweb/` | Flutter web UI |
Gateway note: current dev compose workflows (`make services-up`, `make build-gateways`) use Aurora for card-payout flows (`chain`, `tron`, `aurora`, `tgsettle`). The MNTX gateway codebase is retained separately for Monetix-specific integration.
Gateway note: current dev compose workflows (`make services-up`, `make build-gateways`) use (`chain`, `tron`, `aurora`, `chsettle`). ChimeraSettle is the settlement simulator for test flows; it supports deterministic behavior routing via explicit scenario override and amount buckets. TGSettle remains in-repo as legacy code and is not started by default in dev compose.
## Prerequisites
@@ -87,7 +88,7 @@ make list-services # Show service names, ports, and descriptions
make build-core # discovery, ledger, fees, documents
make build-fx # oracle, ingestor
make build-payments # orchestrator, quotation, methods
make build-gateways # chain, tron, aurora, tgsettle
make build-gateways # chain, tron, aurora, chsettle
make build-api # notification, callbacks, bff
make build-frontend # Flutter web UI
```
@@ -109,6 +110,25 @@ make test-api # Run Go API tests only
make test-frontend # Run Flutter tests only
```
### Backend CI Bypass Tags
Backend Woodpecker module pipelines now run both lint and tests before image build/deploy.
If you intentionally need to bypass checks for a specific commit, include one of these tags in the commit message:
```text
[skip-lint:<service>] # Skip lint for one backend service pipeline
[skip-lint] # Skip lint for all backend service pipelines
[skip-tests:<service>] # Skip tests for one backend service pipeline
[skip-tests] # Skip tests for all backend service pipelines
[skip-autotests:<service>] # Alias for skip-tests:<service>
[skip-autotests] # Alias for skip-tests
[skip-checks:<service>] # Skip both lint and tests for one backend service pipeline
[skip-checks] # Skip both lint and tests for all backend service pipelines
```
`<service>` must match the backend pipeline key used by CI:
`bff`, `callbacks`, `billing_documents`, `billing_fees`, `discovery`, `fx_ingestor`, `fx_oracle`, `gateway_chain`, `gateway_mntx`, `gateway_tgsettle`, `gateway_tron`, `ledger`, `notification`, `payments_methods`, `payments_orchestrator`, `payments_quotation`.
### Update Dependencies
```bash

View File

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

View File

@@ -24,8 +24,6 @@ database:
documents:
issuer:
legal_name: "Sendico Ltd"
legal_address: "12 Market Street, London, UK"
logo_path: "assets/logo.png"
templates:
acceptance_path: "templates/acceptance.tpl"

View File

@@ -24,8 +24,6 @@ database:
documents:
issuer:
legal_name: "Sendico Ltd"
legal_address: "12 Market Street, London, UK"
logo_path: "/app/assets/logo.png"
templates:
acceptance_path: "/app/templates/acceptance.tpl"

View File

@@ -8,14 +8,14 @@ require (
github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
github.com/jung-kurt/gofpdf v1.16.2
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1
google.golang.org/grpc v1.79.2
gopkg.in/yaml.v3 v3.0.1
)
@@ -25,7 +25,7 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
@@ -59,11 +59,11 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11 // indirect

View File

@@ -20,8 +20,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJ
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 h1:3Y4oma5TiV7tT9wa8zRcdoXwZkGz9Q/wxbEUK7cMuAM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
@@ -30,8 +30,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7su
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 h1:+d0SsTvxtIJt4tSJ6wr+jrxEMDa6XeupjRv8H7Qitkk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3/go.mod h1:ROUNFvFWPwBlOu687WJNQ9cPvd2ccpFrnCiA1YGz50o=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
@@ -217,8 +217,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@@ -233,16 +233,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -260,8 +260,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -0,0 +1,116 @@
package content
// Issuer details are intentionally centralized to avoid document text drift.
const (
IssuerLegalName = "SMX Operations Limited"
IssuerLegalAddress = "Room 607, 12/F., Block C, Hong Kong Industrial Centre, 489-491 Castle Peak Road, Lai Chi Kok, Hong Kong"
)
const (
PDFTitleActOfAcceptance = "Act of Acceptance"
DocumentIntegrityHashPrefix = "Document integrity hash: "
)
// AcceptanceTemplateContent contains all static copy used by the acceptance act template.
type AcceptanceTemplateContent struct {
Title string
Subtitle string
MetaDateLabel string
MetaActNumberLabel string
SectionParties string
PartiesIntro string
PartyExecutorLabel string
PartyStatusLabel string
PartyStatusValue string
SectionBasis string
BasisLine1 string
BasisLine2 string
SectionServicesRendered string
ServicesRenderedLine1 string
ServicesRenderedLine2 string
SectionRemuneration string
RemunerationHeaderDesc string
RemunerationHeaderAmount string
RemunerationServicesDesc string
SectionConfirmation string
ConfirmationLine1 string
ConfirmationLine2 string
ConfirmationPaymentLine1 string
ConfirmationPaymentLine2 string
SectionSignatures string
SignatureCustomerLine string
SignatureExecutorLine string
}
var AcceptanceTemplate = AcceptanceTemplateContent{
Title: "ACT OF ACCEPTANCE OF SERVICES",
Subtitle: "under the Public Offer Agreement",
MetaDateLabel: "Date",
MetaActNumberLabel: "Act No",
SectionParties: "PARTIES",
PartiesIntro: "This Act is made between the following Parties.",
PartyExecutorLabel: "Executor",
PartyStatusLabel: "Status",
PartyStatusValue: "Individual",
SectionBasis: "BASIS",
BasisLine1: "This Act is issued pursuant to the Public Offer Agreement",
BasisLine2: "accepted by the Executor by joining the offer.",
SectionServicesRendered: "SERVICES RENDERED",
ServicesRenderedLine1: "The Executor has rendered services to the Customer",
ServicesRenderedLine2: "in accordance with the terms of the Public Offer Agreement.",
SectionRemuneration: "REMUNERATION",
RemunerationHeaderDesc: "Description",
RemunerationHeaderAmount: "Amount",
RemunerationServicesDesc: "Services rendered under the Public Offer Agreement",
SectionConfirmation: "CONFIRMATION",
ConfirmationLine1: "The Customer confirms that the services were rendered properly",
ConfirmationLine2: "and accepted without any claims.",
ConfirmationPaymentLine1: "The remuneration for the services was paid to the Executor",
ConfirmationPaymentLine2: "using the bank card details provided by the Executor.",
SectionSignatures: "SIGNATURES",
SignatureCustomerLine: "Customer ___________________________",
SignatureExecutorLine: "Executor ___________________________",
}
// OperationDocumentContent contains all static copy for operation documents.
type OperationDocumentContent struct {
Title string
Subtitle string
MetaDocumentType string
SectionOperation string
SectionFailure string
RowOrganization string
RowGatewayService string
RowOperationRef string
RowPaymentRef string
RowCode string
RowState string
RowLabel string
RowStartedAtUTC string
RowCompletedAtUTC string
RowAmount string
RowFailureCode string
RowFailureReason string
MissingValuePlaceholder string
}
var OperationDocument = OperationDocumentContent{
Title: "OPERATION BILLING DOCUMENT",
Subtitle: "Gateway operation statement",
MetaDocumentType: "Document Type: Operation",
SectionOperation: "OPERATION DETAILS",
SectionFailure: "FAILURE DETAILS",
RowOrganization: "Organization",
RowGatewayService: "Gateway Service",
RowOperationRef: "Operation Ref",
RowPaymentRef: "Payment Ref",
RowCode: "Code",
RowState: "State",
RowLabel: "Label",
RowStartedAtUTC: "Started At (UTC)",
RowCompletedAtUTC: "Completed At (UTC)",
RowAmount: "Amount",
RowFailureCode: "Failure Code",
RowFailureReason: "Failure Reason",
MissingValuePlaceholder: "n/a",
}

View File

@@ -2,6 +2,7 @@ package docstore
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
@@ -36,8 +37,12 @@ func (s *LocalStore) Save(ctx context.Context, key string, data []byte) error {
return err
}
path := filepath.Join(s.rootPath, filepath.Clean(key))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
path, err := resolveStoragePath(s.rootPath, key)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path))
return err
@@ -56,8 +61,12 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
return nil, err
}
path := filepath.Join(s.rootPath, filepath.Clean(key))
path, err := resolveStoragePath(s.rootPath, key)
if err != nil {
return nil, err
}
//nolint:gosec // path is constrained by resolveStoragePath to stay within configured root.
data, err := os.ReadFile(path)
if err != nil {
s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path))
@@ -69,3 +78,32 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
}
var _ Store = (*LocalStore)(nil)
func resolveStoragePath(rootPath string, key string) (string, error) {
cleanKey := filepath.Clean(strings.TrimSpace(key))
if cleanKey == "" || cleanKey == "." {
return "", merrors.InvalidArgument("docstore: key is required")
}
if filepath.IsAbs(cleanKey) {
return "", merrors.InvalidArgument("docstore: absolute keys are not allowed")
}
rootAbs, err := filepath.Abs(rootPath)
if err != nil {
return "", fmt.Errorf("resolve local store root: %w", err)
}
path := filepath.Join(rootAbs, cleanKey)
pathAbs, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("resolve local store path: %w", err)
}
prefix := rootAbs + string(filepath.Separator)
if pathAbs != rootAbs && !strings.HasPrefix(pathAbs, prefix) {
return "", merrors.InvalidArgument("docstore: key escapes root path")
}
return pathAbs, nil
}

View File

@@ -124,7 +124,11 @@ func (s *S3Store) Load(ctx context.Context, key string) ([]byte, error) {
return nil, err
}
defer obj.Body.Close()
defer func() {
if closeErr := obj.Body.Close(); closeErr != nil {
s.logger.Warn("Failed to close document body", zap.Error(closeErr), zap.String("key", key))
}
}()
return io.ReadAll(obj.Body)
}

View File

@@ -3,18 +3,24 @@ package documents
import (
"strings"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/internal/docstore"
"github.com/tech/sendico/billing/documents/renderer"
)
// Config holds document service settings loaded from YAML.
type Config struct {
Issuer renderer.Issuer `yaml:"issuer"`
Issuer IssuerConfig `yaml:"issuer"`
Templates TemplateConfig `yaml:"templates"`
Protection ProtectionConfig `yaml:"protection"`
Storage docstore.Config `yaml:"storage"`
}
// IssuerConfig defines issuer settings that are environment-specific.
type IssuerConfig struct {
LogoPath string `yaml:"logo_path"`
}
// TemplateConfig defines document template locations.
type TemplateConfig struct {
AcceptancePath string `yaml:"acceptance_path"`
@@ -25,6 +31,14 @@ type ProtectionConfig struct {
OwnerPassword string `yaml:"owner_password"`
}
func (c Config) IssuerDetails() renderer.Issuer {
return renderer.Issuer{
LegalName: content.IssuerLegalName,
LegalAddress: content.IssuerLegalAddress,
LogoPath: c.Issuer.LogoPath,
}
}
func (c Config) AcceptanceTemplatePath() string {
if strings.TrimSpace(c.Templates.AcceptancePath) == "" {
return "templates/acceptance.tpl"

View File

@@ -7,7 +7,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@@ -17,7 +16,6 @@ var (
requestsTotal *prometheus.CounterVec
requestLatency *prometheus.HistogramVec
batchSize prometheus.Histogram
documentBytes *prometheus.HistogramVec
)
@@ -44,16 +42,6 @@ func initMetrics() {
[]string{"call", "status", "doc_type"},
)
batchSize = promauto.NewHistogram(
prometheus.HistogramOpts{
Namespace: "billing",
Subsystem: "documents",
Name: "batch_size",
Help: "Number of payment references in batch resolution requests.",
Buckets: []float64{0, 1, 2, 5, 10, 20, 50, 100, 250, 500},
},
)
documentBytes = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "billing",
@@ -67,18 +55,14 @@ func initMetrics() {
})
}
func observeRequest(call string, docType documentsv1.DocumentType, statusLabel string, took time.Duration) {
typeLabel := docTypeLabel(docType)
requestsTotal.WithLabelValues(call, statusLabel, typeLabel).Inc()
requestLatency.WithLabelValues(call, statusLabel, typeLabel).Observe(took.Seconds())
func observeRequest(call string, documentKind, statusLabel string, took time.Duration) {
kind := docKindLabel(documentKind)
requestsTotal.WithLabelValues(call, statusLabel, kind).Inc()
requestLatency.WithLabelValues(call, statusLabel, kind).Observe(took.Seconds())
}
func observeBatchSize(size int) {
batchSize.Observe(float64(size))
}
func observeDocumentBytes(docType documentsv1.DocumentType, size int) {
documentBytes.WithLabelValues(docTypeLabel(docType)).Observe(float64(size))
func observeDocumentBytes(documentKind string, size int) {
documentBytes.WithLabelValues(docKindLabel(documentKind)).Observe(float64(size))
}
func statusFromError(err error) string {
@@ -100,10 +84,10 @@ func statusFromError(err error) string {
return strings.ToLower(code.String())
}
func docTypeLabel(docType documentsv1.DocumentType) string {
label := docType.String()
func docKindLabel(documentKind string) string {
label := strings.TrimSpace(documentKind)
if label == "" {
return "DOCUMENT_TYPE_UNSPECIFIED"
return "operation"
}
return label

View File

@@ -5,11 +5,11 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/tech/sendico/billing/documents/internal/appversion"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/internal/docstore"
"github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage"
@@ -145,94 +145,6 @@ func (s *Service) Shutdown() {
}
}
func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) {
start := time.Now()
paymentRefs := 0
if req != nil {
paymentRefs = len(req.GetPaymentRefs())
}
logger := s.logger.With(zap.Int("payment_refs", paymentRefs))
defer func() {
statusLabel := statusFromError(err)
observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start))
observeBatchSize(paymentRefs)
itemsCount := 0
if resp != nil {
itemsCount = len(resp.GetItems())
}
fields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Int("items", itemsCount),
}
if err != nil {
logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...)
return
}
logger.Info("BatchResolveDocuments finished", fields...)
}()
_ = ctx
err = status.Error(codes.Unimplemented, "payment-level document flow removed; use GetOperationDocument")
return nil, err
}
func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
start := time.Now()
docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
paymentRef := ""
if req != nil {
docType = req.GetType()
paymentRef = strings.TrimSpace(req.GetPaymentRef())
}
logger := s.logger.With(
zap.String("payment_ref", paymentRef),
zap.String("document_type", docTypeLabel(docType)),
)
defer func() {
statusLabel := statusFromError(err)
observeRequest("get_document", docType, statusLabel, time.Since(start))
if resp != nil {
observeDocumentBytes(docType, len(resp.GetContent()))
}
contentBytes := 0
if resp != nil {
contentBytes = len(resp.GetContent())
}
fields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Int("content_bytes", contentBytes),
}
if err != nil {
logger.Warn("GetDocument failed", append(fields, zap.Error(err))...)
return
}
logger.Info("GetDocument finished", fields...)
}()
_ = ctx
err = status.Error(codes.Unimplemented, "payment-level document flow removed; use GetOperationDocument")
return nil, err
}
func (s *Service) GetOperationDocument(_ context.Context, req *documentsv1.GetOperationDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
start := time.Now()
organizationRef := ""
@@ -253,11 +165,10 @@ func (s *Service) GetOperationDocument(_ context.Context, req *documentsv1.GetOp
defer func() {
statusLabel := statusFromError(err)
docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
observeRequest("get_operation_document", docType, statusLabel, time.Since(start))
observeRequest("get_operation_document", "operation", statusLabel, time.Since(start))
if resp != nil {
observeDocumentBytes(docType, len(resp.GetContent()))
observeDocumentBytes("operation", len(resp.GetContent()))
}
contentBytes := 0
@@ -336,18 +247,6 @@ func (s *Service) startDiscoveryAnnouncer() {
s.announcer.Start()
}
type serviceError string
func (e serviceError) Error() string {
return string(e)
}
var (
errStorageUnavailable = serviceError("documents: storage not initialised")
errDocStoreUnavailable = serviceError("documents: document store not initialised")
errTemplateUnavailable = serviceError("documents: template renderer not initialised")
)
func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, error) {
blocks, err := s.template.Render(snapshot)
if err != nil {
@@ -363,7 +262,7 @@ func (s *Service) generateOperationPDF(snapshot operationSnapshot) ([]byte, stri
func (s *Service) renderPDFWithIntegrity(blocks []renderer.Block) ([]byte, string, error) {
generated := renderer.Renderer{
Issuer: s.config.Issuer,
Issuer: s.config.IssuerDetails(),
OwnerPassword: s.config.Protection.OwnerPassword,
}
@@ -427,39 +326,41 @@ func operationSnapshotFromRequest(req *documentsv1.GetOperationDocumentRequest)
}
func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
documentCopy := content.OperationDocument
rows := [][]string{
{"Organization", snapshot.OrganizationRef},
{"Gateway Service", snapshot.GatewayService},
{"Operation Ref", snapshot.OperationRef},
{"Payment Ref", safeValue(snapshot.PaymentRef)},
{"Code", safeValue(snapshot.OperationCode)},
{"State", safeValue(snapshot.OperationState)},
{"Label", safeValue(snapshot.OperationLabel)},
{"Started At (UTC)", formatSnapshotTime(snapshot.StartedAt)},
{"Completed At (UTC)", formatSnapshotTime(snapshot.CompletedAt)},
{documentCopy.RowOrganization, snapshot.OrganizationRef},
{documentCopy.RowGatewayService, snapshot.GatewayService},
{documentCopy.RowOperationRef, snapshot.OperationRef},
{documentCopy.RowPaymentRef, safeValue(snapshot.PaymentRef)},
{documentCopy.RowCode, safeValue(snapshot.OperationCode)},
{documentCopy.RowState, safeValue(snapshot.OperationState)},
{documentCopy.RowLabel, safeValue(snapshot.OperationLabel)},
{documentCopy.RowStartedAtUTC, formatSnapshotTime(snapshot.StartedAt)},
{documentCopy.RowCompletedAtUTC, formatSnapshotTime(snapshot.CompletedAt)},
}
if snapshot.Amount != "" || snapshot.Currency != "" {
rows = append(rows, []string{"Amount", strings.TrimSpace(strings.TrimSpace(snapshot.Amount) + " " + strings.TrimSpace(snapshot.Currency))})
rows = append(rows, []string{documentCopy.RowAmount, strings.TrimSpace(strings.TrimSpace(snapshot.Amount) + " " + strings.TrimSpace(snapshot.Currency))})
}
blocks := []renderer.Block{
{
Tag: renderer.TagTitle,
Lines: []string{"OPERATION BILLING DOCUMENT"},
Lines: []string{documentCopy.Title},
},
{
Tag: renderer.TagSubtitle,
Lines: []string{"Gateway operation statement"},
Lines: []string{documentCopy.Subtitle},
},
{
Tag: renderer.TagMeta,
Lines: []string{
"Document Type: Operation",
documentCopy.MetaDocumentType,
},
},
{
Tag: renderer.TagSection,
Lines: []string{"OPERATION DETAILS"},
Lines: []string{documentCopy.SectionOperation},
},
{
Tag: renderer.TagKV,
@@ -469,12 +370,12 @@ func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
if snapshot.FailureCode != "" || snapshot.FailureReason != "" {
blocks = append(blocks,
renderer.Block{Tag: renderer.TagSection, Lines: []string{"FAILURE DETAILS"}},
renderer.Block{Tag: renderer.TagSection, Lines: []string{documentCopy.SectionFailure}},
renderer.Block{
Tag: renderer.TagKV,
Rows: [][]string{
{"Failure Code", safeValue(snapshot.FailureCode)},
{"Failure Reason", safeValue(snapshot.FailureReason)},
{documentCopy.RowFailureCode, safeValue(snapshot.FailureCode)},
{documentCopy.RowFailureReason, safeValue(snapshot.FailureReason)},
},
},
)
@@ -485,7 +386,7 @@ func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
func formatSnapshotTime(value time.Time) string {
if value.IsZero() {
return "n/a"
return content.OperationDocument.MissingValuePlaceholder
}
return value.UTC().Format(time.RFC3339)
@@ -494,7 +395,7 @@ func formatSnapshotTime(value time.Time) string {
func safeValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "n/a"
return content.OperationDocument.MissingValuePlaceholder
}
return trimmed
@@ -535,50 +436,3 @@ func sanitizeFilenameComponent(value string) string {
return strings.Trim(b.String(), "_")
}
func toProtoTypes(types []model.DocumentType) []documentsv1.DocumentType {
if len(types) == 0 {
return nil
}
result := make([]documentsv1.DocumentType, 0, len(types))
for _, t := range types {
result = append(result, t.Proto())
}
return result
}
func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string {
suffix := "document.pdf"
switch docType {
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
suffix = "act.pdf"
case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE:
suffix = "invoice.pdf"
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
suffix = "receipt.pdf"
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
// default suffix used
}
return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix))
}
func documentFilename(docType documentsv1.DocumentType, paymentRef string) string {
name := "document"
switch docType {
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
name = "act"
case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE:
name = "invoice"
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
name = "receipt"
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
// default name used
}
return fmt.Sprintf("%s_%s.pdf", name, paymentRef)
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage"
"github.com/tech/sendico/billing/documents/storage/model"
@@ -53,38 +54,6 @@ func (s *stubDocumentsStore) ListByPaymentRefs(_ context.Context, _ []string) ([
var _ storage.DocumentsStore = (*stubDocumentsStore)(nil)
type memDocStore struct {
data map[string][]byte
saveCount int
loadCount int
}
func newMemDocStore() *memDocStore {
return &memDocStore{data: map[string][]byte{}}
}
func (m *memDocStore) Save(_ context.Context, key string, data []byte) error {
m.saveCount++
copyData := make([]byte, len(data))
copy(copyData, data)
m.data[key] = copyData
return nil
}
func (m *memDocStore) Load(_ context.Context, key string) ([]byte, error) {
m.loadCount++
data := m.data[key]
copyData := make([]byte, len(data))
copy(copyData, data)
return copyData, nil
}
func (m *memDocStore) Counts() (int, int) {
return m.saveCount, m.loadCount
}
type stubTemplate struct {
blocks []renderer.Block
calls int
@@ -112,15 +81,7 @@ func TestGenerateActPDF_IdempotentAndHashed(t *testing.T) {
},
}
cfg := Config{
Issuer: renderer.Issuer{
LegalName: "Sendico Ltd",
LegalAddress: "12 Market Street, London, UK",
},
}
svc := NewService(zap.NewNop(), nil, nil,
WithConfig(cfg),
WithTemplateRenderer(tmpl),
)
@@ -164,7 +125,7 @@ func TestGenerateActPDF_IdempotentAndHashed(t *testing.T) {
}
func extractFooterHash(pdf []byte) string {
prefix := []byte("Document integrity hash: ")
prefix := []byte(content.DocumentIntegrityHashPrefix)
idx := bytes.Index(pdf, prefix)
if idx == -1 {
@@ -191,11 +152,7 @@ func isHexDigit(b byte) bool {
}
func TestGetOperationDocument_GeneratesPDF(t *testing.T) {
svc := NewService(zap.NewNop(), nil, nil, WithConfig(Config{
Issuer: renderer.Issuer{
LegalName: "Sendico Ltd",
},
}))
svc := NewService(zap.NewNop(), nil, nil)
resp, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
OrganizationRef: "org-1",

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage/model"
)
@@ -17,7 +18,13 @@ type templateRenderer struct {
tpl *template.Template
}
type acceptanceTemplateData struct {
model.ActSnapshot
Content content.AcceptanceTemplateContent
}
func newTemplateRenderer(path string) (*templateRenderer, error) {
//nolint:gosec // template file path is provided by trusted service configuration.
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read template: %w", err)
@@ -38,7 +45,12 @@ func newTemplateRenderer(path string) (*templateRenderer, error) {
func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) {
var buf bytes.Buffer
if err := r.tpl.Execute(&buf, snapshot); err != nil {
data := acceptanceTemplateData{
ActSnapshot: snapshot,
Content: content.AcceptanceTemplate,
}
if err := r.tpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("execute template: %w", err)
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage/model"
)
@@ -42,7 +43,7 @@ func TestTemplateRenderer_Render(t *testing.T) {
t.Fatalf("expected title block")
}
if !slices.Contains(title.Lines, "ACT OF ACCEPTANCE OF SERVICES") {
if !slices.Contains(title.Lines, content.AcceptanceTemplate.Title) {
t.Fatalf("expected title content not found")
}
@@ -54,7 +55,7 @@ func TestTemplateRenderer_Render(t *testing.T) {
foundExecutor := false
for _, row := range kv.Rows {
if len(row) >= 2 && row[0] == "Executor" && row[1] == snapshot.ExecutorFullName {
if len(row) >= 2 && row[0] == content.AcceptanceTemplate.PartyExecutorLabel && row[1] == snapshot.ExecutorFullName {
foundExecutor = true
break

View File

@@ -6,6 +6,7 @@ import (
"strings"
"github.com/jung-kurt/gofpdf"
"github.com/tech/sendico/billing/documents/internal/content"
)
const (
@@ -28,7 +29,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
pdf.SetAutoPageBreak(true, pageMarginBottom)
pdf.SetCompression(false)
pdf.SetAuthor(r.Issuer.LegalName, false)
pdf.SetTitle("Act of Acceptance", false)
pdf.SetTitle(content.PDFTitleActOfAcceptance, false)
owner := strings.TrimSpace(r.OwnerPassword)
if owner != "" {
@@ -39,7 +40,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
pdf.SetY(-15)
pdf.SetFont("Helvetica", "", 8)
footer := "Document integrity hash: " + footerHash
footer := content.DocumentIntegrityHashPrefix + footerHash
pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "")
})

View File

@@ -6,6 +6,8 @@ import (
"strings"
"testing"
"unicode/utf16"
"github.com/tech/sendico/billing/documents/internal/content"
)
func TestRenderer_RenderContainsText(t *testing.T) {
@@ -31,7 +33,7 @@ func TestRenderer_RenderContainsText(t *testing.T) {
t.Fatalf("expected PDF bytes")
}
checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", "Document integrity hash"}
checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", strings.TrimSpace(strings.TrimSuffix(content.DocumentIntegrityHashPrefix, ": "))}
for _, token := range checks {
if !containsPDFText(pdfBytes, token) {
@@ -100,7 +102,7 @@ func encodeUTF16BE(text string, withBOM bool) []byte {
}
for _, v := range encoded {
out = append(out, byte(v>>8), byte(v))
out = append(out, byte(v>>8), byte(v&0x00FF))
}
return out

View File

@@ -6,14 +6,13 @@ import (
"github.com/shopspring/decimal"
"github.com/tech/sendico/pkg/db/storable"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
)
const (
DocumentRecordsCollection = "document_records"
)
// DocumentType mirrors the protobuf enum but stores string names for Mongo compatibility.
// DocumentType represents document kinds cached in storage.
type DocumentType string
const (
@@ -23,24 +22,6 @@ const (
DocumentTypeReceipt DocumentType = "DOCUMENT_TYPE_RECEIPT"
)
// DocumentTypeFromProto converts a protobuf enum to the storage representation.
func DocumentTypeFromProto(t documentsv1.DocumentType) DocumentType {
if name, ok := documentsv1.DocumentType_name[int32(t)]; ok {
return DocumentType(name)
}
return DocumentTypeUnspecified
}
// Proto converts the storage representation to a protobuf enum.
func (t DocumentType) Proto() documentsv1.DocumentType {
if value, ok := documentsv1.DocumentType_value[string(t)]; ok {
return documentsv1.DocumentType(value)
}
return documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
}
// ActSnapshot captures the immutable data needed to generate an acceptance act.
type ActSnapshot struct {
PaymentID string `bson:"paymentId" json:"paymentId"`

View File

@@ -2,66 +2,66 @@
#title
ACT OF ACCEPTANCE OF SERVICES
{{ .Content.Title }}
#subtitle
under the Public Offer Agreement
{{ .Content.Subtitle }}
#meta
Date: {{ date .Date }}
Act No: {{ .PaymentID }}
{{ .Content.MetaDateLabel }}: {{ date .Date }}
{{ .Content.MetaActNumberLabel }}: {{ .PaymentID }}
#section
PARTIES
{{ .Content.SectionParties }}
#text
This Act is made between the following Parties.
{{ .Content.PartiesIntro }}
#kv
Executor | {{ .ExecutorFullName }}
Status | Individual
{{ .Content.PartyExecutorLabel }} | {{ .ExecutorFullName }}
{{ .Content.PartyStatusLabel }} | {{ .Content.PartyStatusValue }}
#section
BASIS
{{ .Content.SectionBasis }}
#text
This Act is issued pursuant to the Public Offer Agreement
accepted by the Executor by joining the offer.
{{ .Content.BasisLine1 }}
{{ .Content.BasisLine2 }}
#section
SERVICES RENDERED
{{ .Content.SectionServicesRendered }}
#text
The Executor has rendered services to the Customer
in accordance with the terms of the Public Offer Agreement.
{{ .Content.ServicesRenderedLine1 }}
{{ .Content.ServicesRenderedLine2 }}
#section
REMUNERATION
{{ .Content.SectionRemuneration }}
#table
Description | Amount
Services rendered under the Public Offer Agreement | {{ money .Amount .Currency }}
{{ .Content.RemunerationHeaderDesc }} | {{ .Content.RemunerationHeaderAmount }}
{{ .Content.RemunerationServicesDesc }} | {{ money .Amount .Currency }}
#section
CONFIRMATION
{{ .Content.SectionConfirmation }}
#text
The Customer confirms that the services were rendered properly
and accepted without any claims.
{{ .Content.ConfirmationLine1 }}
{{ .Content.ConfirmationLine2 }}
The remuneration for the services was paid to the Executor
using the bank card details provided by the Executor.
{{ .Content.ConfirmationPaymentLine1 }}
{{ .Content.ConfirmationPaymentLine2 }}
#section
SIGNATURES
{{ .Content.SectionSignatures }}
#sign
Customer ___________________________
{{ .Content.SignatureCustomerLine }}
Executor ___________________________
{{ .Content.SignatureExecutorLine }}

View File

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

View File

@@ -10,7 +10,7 @@ require (
github.com/tech/sendico/fx/oracle v0.0.0
github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1
google.golang.org/grpc v1.79.2
gopkg.in/yaml.v3 v3.0.1
)
@@ -44,11 +44,11 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11

View File

@@ -168,8 +168,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@@ -183,16 +183,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -210,8 +210,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

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

View File

@@ -37,13 +37,13 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -168,8 +168,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@@ -183,16 +183,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -210,8 +210,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -0,0 +1,47 @@
version: "2"
linters:
default: none
enable:
- bodyclose
- canonicalheader
- copyloopvar
- durationcheck
- errcheck
- errchkjson
- errname
- errorlint
- gosec
- govet
- ineffassign
- nilerr
- nilnesserr
- nilnil
- noctx
- rowserrcheck
- sqlclosecheck
- staticcheck
- unconvert
- wastedassign
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gochecknoinits
- gomoddirectives
- wrapcheck
- cyclop
- dupl
- funlen
- gocognit
- gocyclo
- ireturn
- lll
- mnd
- nestif
- nlreturn
- noinlineerr
- paralleltest
- tagliatelle
- testpackage
- varnamelen
- wsl_v5

View File

@@ -18,7 +18,7 @@ require (
github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.4.0
@@ -38,7 +38,7 @@ require (
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
golang.org/x/net v0.51.0
google.golang.org/grpc v1.79.1
google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
moul.io/chizap v1.0.3
@@ -59,7 +59,7 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
@@ -153,11 +153,11 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)

View File

@@ -22,8 +22,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJ
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 h1:3Y4oma5TiV7tT9wa8zRcdoXwZkGz9Q/wxbEUK7cMuAM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
@@ -32,8 +32,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7su
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 h1:+d0SsTvxtIJt4tSJ6wr+jrxEMDa6XeupjRv8H7Qitkk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3/go.mod h1:ROUNFvFWPwBlOu687WJNQ9cPvd2ccpFrnCiA1YGz50o=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
@@ -324,8 +324,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -353,8 +353,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -370,8 +370,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
@@ -382,8 +382,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -403,8 +403,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -159,7 +159,7 @@ func (s *service) VerifyAccount(
token, err := s.vdb.Create(
ctx,
verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, "").
WithTTL(time.Duration(time.Hour*24)),
WithTTL(time.Hour * 24),
)
if err != nil {
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
@@ -238,7 +238,7 @@ func (s *service) ResetPassword(
return s.vdb.Create(
ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, "").
WithTTL(time.Duration(time.Hour*1)),
WithTTL(time.Hour),
)
}
@@ -250,7 +250,7 @@ func (s *service) UpdateLogin(
return s.vdb.Create(
ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin).
WithTTL(time.Duration(time.Hour*1)),
WithTTL(time.Hour),
)
}

View File

@@ -164,6 +164,7 @@ func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) {
func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
if old == nil {
//nolint:nilnil // Nil legacy endpoint means no endpoint provided.
return nil, nil
}
@@ -202,6 +203,7 @@ func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint,
func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) {
if new == nil {
//nolint:nilnil // Nil endpoint DTO means no endpoint provided.
return nil, nil
}

View File

@@ -344,7 +344,7 @@ func toPaymentOperation(step *orchestrationv2.StepExecution, quote *quotationv2.
Amount: amount,
ConvertedAmount: convertedAmount,
OperationRef: operationRef,
Gateway: string(gateway),
Gateway: gateway,
StartedAt: timestampAsTime(step.GetStartedAt()),
CompletedAt: timestampAsTime(step.GetCompletedAt()),
}
@@ -456,9 +456,9 @@ func gatewayTypeFromInstanceID(raw string) mservice.Type {
return ""
}
switch mservice.Type(value) {
switch value {
case mservice.ChainGateway, mservice.TronGateway, mservice.MntxGateway, mservice.PaymentGateway, mservice.TgSettle, mservice.Ledger:
return mservice.Type(value)
return value
}
switch {

View File

@@ -110,7 +110,7 @@ func Account2ClaimsForClient(a *model.Account, expiration int, clientID string)
paramNameName: t.Name,
paramNameLocale: t.Locale,
paramNameClientID: t.ClientID,
paramNameExpiration: int64(t.Expiration.Unix()),
paramNameExpiration: t.Expiration.Unix(),
paramNamePending: t.Pending,
}
}

View File

@@ -26,11 +26,11 @@ const (
var (
ledgerDiscoveryServiceNames = []string{
"LEDGER",
string(mservice.Ledger),
mservice.Ledger,
}
paymentOrchestratorDiscoveryServiceNames = []string{
"PAYMENTS_ORCHESTRATOR",
string(mservice.PaymentOrchestrator),
mservice.PaymentOrchestrator,
}
paymentQuotationDiscoveryServiceNames = []string{
"PAYMENTS_QUOTATION",
@@ -41,7 +41,7 @@ var (
paymentMethodsDiscoveryServiceNames = []string{
"PAYMENTS_METHODS",
"PAYMENT_METHODS",
string(mservice.PaymentMethods),
mservice.PaymentMethods,
}
)
@@ -339,13 +339,13 @@ func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork
func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return discoveryEndpoint{}, fmt.Errorf("Invoke uri is empty")
return discoveryEndpoint{}, fmt.Errorf("invoke uri is empty")
}
// Without a scheme we expect a plain host:port target.
if !strings.Contains(raw, "://") {
if _, _, err := net.SplitHostPort(raw); err != nil {
return discoveryEndpoint{}, fmt.Errorf("Invoke uri must include host:port: %w", err)
return discoveryEndpoint{}, fmt.Errorf("invoke uri must include host:port: %w", err)
}
return discoveryEndpoint{
address: raw,
@@ -363,7 +363,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
case "grpc":
address := strings.TrimSpace(parsed.Host)
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
return discoveryEndpoint{}, fmt.Errorf("Grpc invoke uri must include host:port: %w", splitErr)
return discoveryEndpoint{}, fmt.Errorf("grpc invoke uri must include host:port: %w", splitErr)
}
return discoveryEndpoint{
address: address,
@@ -373,7 +373,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
case "grpcs":
address := strings.TrimSpace(parsed.Host)
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
return discoveryEndpoint{}, fmt.Errorf("Grpcs invoke uri must include host:port: %w", splitErr)
return discoveryEndpoint{}, fmt.Errorf("grpcs invoke uri must include host:port: %w", splitErr)
}
return discoveryEndpoint{
address: address,
@@ -388,7 +388,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
raw: raw,
}, nil
default:
return discoveryEndpoint{}, fmt.Errorf("Unsupported invoke uri scheme: %s", parsed.Scheme)
return discoveryEndpoint{}, fmt.Errorf("unsupported invoke uri scheme: %s", parsed.Scheme)
}
}

View File

@@ -43,6 +43,7 @@ func (d *DispatcherImpl) dispatchMessage(ctx context.Context, conn *websocket.Co
}
func (d *DispatcherImpl) handle(w http.ResponseWriter, r *http.Request) {
//nolint:contextcheck // websocket.Handler callback signature does not carry request context.
websocket.Handler(func(conn *websocket.Conn) {
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(d.timeout)*time.Second)
defer cancel()

View File

@@ -71,6 +71,7 @@ func GetOptionalParam[T any](logger mlogger.Logger, r *http.Request, key string,
vals := r.URL.Query()
s := vals.Get(key)
if s == "" {
//nolint:nilnil // Missing optional query parameter is represented as (nil, nil).
return nil, nil
}

View File

@@ -1,17 +1,17 @@
package mutil
import (
"context"
"net/http"
"testing"
"github.com/tech/sendico/pkg/mlogger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestGetOptionalBoolParam(t *testing.T) {
logger := mlogger.Logger(zap.NewNop())
logger := zap.NewNop()
tests := []struct {
name string
@@ -47,7 +47,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
require.NoError(t, err)
result, err := GetOptionalBoolParam(logger, req, "param")
@@ -69,7 +69,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
}
func TestGetOptionalInt64Param(t *testing.T) {
logger := mlogger.Logger(zap.NewNop())
logger := zap.NewNop()
tests := []struct {
name string
@@ -111,7 +111,7 @@ func TestGetOptionalInt64Param(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil)
req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
require.NoError(t, err)
result, err := GetOptionalInt64Param(logger, req, "param")

View File

@@ -114,7 +114,7 @@ func (a *AccountAPI) deleteAll(r *http.Request, account *model.Account, token *s
a.logger.Warn("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
return nil, err
}
return nil, nil
return struct{}{}, nil
}); err != nil {
a.logger.Warn("Failed to execute delete transaction", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
return response.Auto(a.logger, a.Name(), err)

View File

@@ -192,5 +192,5 @@ func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *mod
// Don't fail the transaction if token revocation fails, but log it
}
return nil, nil
return struct{}{}, nil
}

View File

@@ -133,12 +133,7 @@ func TestPasswordValidationLogic(t *testing.T) {
for _, password := range invalidPasswords {
t.Run(password, func(t *testing.T) {
// Test that invalid passwords fail at least one requirement
isValid := true
// Check length
if len(password) < 8 {
isValid = false
}
isValid := len(password) >= 8
// Check for digit
hasDigit := false

View File

@@ -276,7 +276,7 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef bs
for resource, granted := range required {
if !granted {
a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", string(resource)))
a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", resource))
}
}

View File

@@ -126,7 +126,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
require.NoError(t, err)
// Create HTTP request
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json")
// Parse the request body
@@ -162,7 +162,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
t.Run("InvalidJSONRequest", func(t *testing.T) {
invalidJSON := `{"account": {"login": "test@example.com", "password": "invalid json structure`
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON))
req.Header.Set("Content-Type", "application/json")
var parsedRequest srequest.Signup

View File

@@ -37,7 +37,7 @@ func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessTok
if err := a.applySigningSecretMutation(ctx, *account.GetID(), *callback.GetID(), mutation); err != nil {
return nil, err
}
return nil, nil
return struct{}{}, nil
}); err != nil {
a.Logger.Warn("Failed to create callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)

View File

@@ -49,7 +49,7 @@ func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessTok
if err := a.applySigningSecretMutation(ctx, *account.GetID(), callbackRef, mutation); err != nil {
return nil, err
}
return nil, nil
return struct{}{}, nil
}); err != nil {
a.Logger.Warn("Failed to update callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/domainprovider"
@@ -34,7 +35,10 @@ func (storage *LocalStorage) Delete(ctx context.Context, objID string) error {
default:
}
filePath := filepath.Join(storage.storageDir, objID)
filePath, err := storage.resolvePath(objID)
if err != nil {
return err
}
if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) {
storage.logger.Debug("File not found", zap.String("obj_ref", objID))
@@ -54,7 +58,11 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
default:
}
filePath := filepath.Join(storage.storageDir, objID)
filePath, err := storage.resolvePath(objID)
if err != nil {
return "", err
}
//nolint:gosec // File path is resolved and constrained to storage root.
dst, err := os.Create(filePath)
if err != nil {
storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID))
@@ -78,7 +86,9 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
}
case <-ctx.Done():
// Context was cancelled, clean up the partial file
os.Remove(filePath)
if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) {
storage.logger.Warn("Failed to remove partially written file", zap.Error(removeErr), zap.String("obj_ref", objID))
}
return "", ctx.Err()
}
@@ -93,7 +103,10 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
default:
}
filePath := filepath.Join(storage.storageDir, objRef)
filePath, err := storage.resolvePath(objRef)
if err != nil {
return response.Internal(storage.logger, storage.service, err)
}
if _, err := os.Stat(filePath); err != nil {
storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef))
return response.Internal(storage.logger, storage.service, err)
@@ -117,7 +130,7 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
func ensureDir(dirName string) error {
info, err := os.Stat(dirName)
if os.IsNotExist(err) {
return os.MkdirAll(dirName, 0o755)
return os.MkdirAll(dirName, 0o750)
}
if err != nil {
return err
@@ -128,6 +141,24 @@ func ensureDir(dirName string) error {
return nil
}
func (storage *LocalStorage) resolvePath(objID string) (string, error) {
objID = strings.TrimSpace(objID)
if objID == "" {
return "", merrors.InvalidArgument("obj_ref is required", "obj_ref")
}
filePath := filepath.Join(storage.storageDir, objID)
relPath, err := filepath.Rel(storage.storageDir, filePath)
if err != nil {
return "", merrors.InternalWrap(err, "failed to resolve local file path")
}
if relPath == "." || strings.HasPrefix(relPath, "..") {
return "", merrors.InvalidArgument("obj_ref is invalid", "obj_ref")
}
return filePath, nil
}
func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) {
dir := filepath.Join(cfg.RootPath, directory)
if err := ensureDir(dir); err != nil {

View File

@@ -55,7 +55,7 @@ func setupTestStorage(t *testing.T) (*LocalStorage, string, func()) {
// Return cleanup function
cleanup := func() {
os.RemoveAll(tempDir)
require.NoError(t, os.RemoveAll(tempDir))
}
return storage, tempDir, cleanup
@@ -81,7 +81,7 @@ func setupBenchmarkStorage(b *testing.B) (*LocalStorage, string, func()) {
// Return cleanup function
cleanup := func() {
os.RemoveAll(tempDir)
require.NoError(b, os.RemoveAll(tempDir))
}
return storage, tempDir, cleanup
@@ -138,6 +138,7 @@ func TestLocalStorage_Save(t *testing.T) {
// Verify file was actually saved
filePath := filepath.Join(tempDir, tt.objID)
//nolint:gosec // Test-controlled path inside temporary directory.
content, err := os.ReadFile(filePath)
assert.NoError(t, err)
assert.Equal(t, tt.content, string(content))
@@ -186,7 +187,7 @@ func TestLocalStorage_Delete(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
tests := []struct {
@@ -232,7 +233,7 @@ func TestLocalStorage_Delete_ContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
// Create a context that's already cancelled
@@ -256,7 +257,7 @@ func TestLocalStorage_Get(t *testing.T) {
// Create a test file
testContent := "test file content"
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte(testContent), 0o644)
err := os.WriteFile(testFile, []byte(testContent), 0o600)
require.NoError(t, err)
tests := []struct {
@@ -285,7 +286,7 @@ func TestLocalStorage_Get(t *testing.T) {
handler := storage.Get(ctx, tt.objID)
// Create test request
req := httptest.NewRequest("GET", "/files/"+tt.objID, nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/"+tt.objID, nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
@@ -304,7 +305,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
// Create a context that's already cancelled
@@ -314,7 +315,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
handler := storage.Get(ctx, "test.txt")
// Create test request
req := httptest.NewRequest("GET", "/files/test.txt", nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
@@ -328,14 +329,14 @@ func TestLocalStorage_Get_RequestContextCancellation(t *testing.T) {
// Create a test file
testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644)
err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err)
ctx := context.Background()
handler := storage.Get(ctx, "test.txt")
// Create test request with cancelled context
req := httptest.NewRequest("GET", "/files/test.txt", nil)
req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
reqCtx, cancel := context.WithCancel(req.Context())
req = req.WithContext(reqCtx)
cancel() // Cancel the request context
@@ -352,7 +353,9 @@ func TestCreateLocalFileStorage(t *testing.T) {
// Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "storage_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
logger := zap.NewNop()
cfg := config.LocalFSSConfig{
@@ -372,10 +375,12 @@ func TestCreateLocalFileStorage_InvalidPath(t *testing.T) {
// Build a deterministic failure case: the target path already exists as a file.
tempDir, err := os.MkdirTemp("", "storage_invalid_path_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
fileAtTargetPath := filepath.Join(tempDir, "test")
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o644)
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o600)
require.NoError(t, err)
logger := zap.NewNop()
@@ -426,7 +431,7 @@ func TestLocalStorage_ConcurrentOperations(t *testing.T) {
// Create files to delete
for i := 0; i < 5; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644)
err := os.WriteFile(filePath, []byte("content"), 0o600)
require.NoError(t, err)
}
@@ -536,7 +541,7 @@ func BenchmarkLocalStorage_Delete(b *testing.B) {
// Pre-create files for deletion
for i := 0; i < b.N; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644)
err := os.WriteFile(filePath, []byte("content"), 0o600)
if err != nil {
b.Fatal(err)
}

View File

@@ -97,7 +97,9 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
}
func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := srequest.CreateLedgerAccount{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {

View File

@@ -46,7 +46,7 @@ func (a *ProtectedAPI[T]) archive(r *http.Request, account *model.Account, acces
ctx := r.Context()
_, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
return nil, a.DB.SetArchived(r.Context(), *account.GetID(), organizationRef, objectRef, *archived, *cascade)
return nil, a.DB.SetArchived(ctx, *account.GetID(), organizationRef, objectRef, *archived, *cascade)
})
if err != nil {
a.Logger.Warn("Failed to change archive property", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r),

View File

@@ -69,7 +69,7 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun
op, err := a.fetchGatewayOperation(r.Context(), gateway.InvokeURI, operationRef)
if err != nil {
a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", string(gatewayService)), zap.String("operation_ref", operationRef))
a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
return documentErrorResponse(a.logger, a.Name(), err)
}
@@ -77,7 +77,7 @@ func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Accoun
docResp, err := a.fetchOperationDocument(r.Context(), service.InvokeURI, req)
if err != nil {
a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", string(gatewayService)), zap.String("operation_ref", operationRef))
a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
return documentErrorResponse(a.logger, a.Name(), err)
}
@@ -154,6 +154,7 @@ func operationDocumentResponse(logger mlogger.Logger, source mservice.Type, docR
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
w.WriteHeader(http.StatusOK)
//nolint:gosec // Binary payload is served as attachment with explicit content type.
if _, err := w.Write(docResp.GetContent()); err != nil {
logger.Warn("Failed to write document response", zap.Error(err))
}
@@ -167,15 +168,15 @@ func normalizeGatewayService(raw string) mservice.Type {
}
switch value {
case string(mservice.ChainGateway):
case mservice.ChainGateway:
return mservice.ChainGateway
case string(mservice.TronGateway):
case mservice.TronGateway:
return mservice.TronGateway
case string(mservice.MntxGateway):
case mservice.MntxGateway:
return mservice.MntxGateway
case string(mservice.PaymentGateway):
case mservice.PaymentGateway:
return mservice.PaymentGateway
case string(mservice.TgSettle):
case mservice.TgSettle:
return mservice.TgSettle
default:
return ""
@@ -219,7 +220,11 @@ func (a *PaymentAPI) fetchOperationDocument(ctx context.Context, invokeURI strin
if err != nil {
return nil, merrors.InternalWrap(err, "dial billing documents")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close billing documents connection", zap.Error(closeErr))
}
}()
client := documentsv1.NewDocumentServiceClient(conn)
@@ -234,7 +239,11 @@ func (a *PaymentAPI) fetchGatewayOperation(ctx context.Context, invokeURI, opera
if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway connector")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connector connection", zap.Error(closeErr))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)
@@ -307,7 +316,7 @@ func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService m
func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest {
req := &documentsv1.GetOperationDocumentRequest{
OrganizationRef: strings.TrimSpace(organizationRef),
GatewayService: string(gatewayService),
GatewayService: gatewayService,
OperationRef: firstNonEmpty(strings.TrimSpace(op.GetOperationRef()), strings.TrimSpace(requestedOperationRef)),
OperationCode: strings.TrimSpace(op.GetType().String()),
OperationLabel: operationLabel(op.GetType()),

View File

@@ -103,6 +103,7 @@ func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error)
}
if cursor == "" && !hasLimit {
//nolint:nilnil // Absent pagination params mean no page request should be sent downstream.
return nil, nil
}
@@ -189,6 +190,7 @@ func firstNonEmpty(values ...string) string {
func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
//nolint:nilnil // Empty timestamp filter is represented as (nil, nil).
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, trimmed)

View File

@@ -102,7 +102,9 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
}
func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.InitiatePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {

View File

@@ -68,7 +68,7 @@ func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
func invokeInitiateByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/by-quote", bytes.NewBufferString(body))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-quote", bytes.NewBufferString(body))
routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))

View File

@@ -63,7 +63,9 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
}
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.InitiatePayments{}
decoder := json.NewDecoder(r.Body)

View File

@@ -10,7 +10,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
@@ -118,7 +117,7 @@ func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefsField(t *testing.T)
func newBatchAPI(exec executionClient) *PaymentAPI {
return &PaymentAPI{
logger: mlogger.Logger(zap.NewNop()),
logger: zap.NewNop(),
execution: exec,
enf: fakeEnforcerForBatch{allowed: true},
oph: mutil.CreatePH(mservice.Organizations),
@@ -129,7 +128,7 @@ func newBatchAPI(exec executionClient) *PaymentAPI {
func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/by-multiquote", bytes.NewBufferString(body))
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-multiquote", bytes.NewBufferString(body))
routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))
@@ -177,6 +176,7 @@ func (f fakeEnforcerForBatch) Enforce(context.Context, bson.ObjectID, bson.Objec
}
func (fakeEnforcerForBatch) EnforceBatch(context.Context, []model.PermissionBoundStorable, bson.ObjectID, model.Action) (map[bson.ObjectID]bool, error) {
//nolint:nilnil // Test stub does not provide batch permissions map.
return nil, nil
}

View File

@@ -126,7 +126,9 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
}
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.QuotePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
@@ -140,7 +142,9 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
}
func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) {
defer r.Body.Close()
defer func() {
_ = r.Body.Close()
}()
payload := &srequest.QuotePayments{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {

View File

@@ -256,6 +256,7 @@ func (c *grpcQuotationClient) callContext(ctx context.Context) (context.Context,
if timeout <= 0 {
timeout = 3 * time.Second
}
//nolint:gosec // Caller receives cancel func and defers it in every call path.
return context.WithTimeout(ctx, timeout)
}
@@ -271,7 +272,7 @@ func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error {
if err != nil {
return err
}
client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name()))
client, err := discovery.NewClient(a.logger, broker, nil, a.Name())
if err != nil {
return err
}

View File

@@ -90,5 +90,5 @@ func (a *PermissionsAPI) changePoliciesImp(
}
}
return nil, nil
return struct{}{}, nil
}

View File

@@ -223,6 +223,7 @@ func (a *WalletAPI) queryBalanceFromGateways(ctx context.Context, gateways []dis
a.logger.Debug("Wallet balance fan-out completed without result",
zap.String("organization_ref", organizationRef),
zap.String("wallet_ref", walletRef))
//nolint:nilnil // No gateway returned a balance and no hard error occurred.
return nil, nil
}
@@ -238,7 +239,11 @@ func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.G
if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -173,7 +173,11 @@ func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery
if err != nil {
return "", merrors.InternalWrap(err, "dial gateway")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -226,7 +226,11 @@ func (a *WalletAPI) queryGateway(ctx context.Context, gateway discovery.GatewayS
if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway")
}
defer conn.Close()
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -64,18 +64,21 @@ func (a *WalletAPI) rememberWalletRoute(ctx context.Context, organizationRef str
func (a *WalletAPI) walletRoute(ctx context.Context, organizationRef string, walletRef string) (*model.ChainWalletRoute, error) {
if a.routes == nil {
//nolint:nilnil // Routing cache is optional and may be disabled.
return nil, nil
}
walletRef = strings.TrimSpace(walletRef)
organizationRef = strings.TrimSpace(organizationRef)
if walletRef == "" || organizationRef == "" {
//nolint:nilnil // Missing route keys mean no cached route.
return nil, nil
}
route, err := a.routes.Get(ctx, organizationRef, walletRef)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
//nolint:nilnil // Route not found in cache.
return nil, nil
}
return nil, err

View File

@@ -129,7 +129,7 @@ func (a *WalletAPI) initDiscoveryClient(cfg *eapi.Config) error {
if err != nil {
return err
}
client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name()))
client, err := discovery.NewClient(a.logger, broker, nil, a.Name())
if err != nil {
return err
}

View File

@@ -9,8 +9,8 @@ import (
)
// generate translations
// go:generate Users/stephandeshevikh/go/bin/go18n extract
// go:generate Users/stephandeshevikh/go/bin/go18n merge
//go:generate Users/stephandeshevikh/go/bin/go18n extract
//go:generate Users/stephandeshevikh/go/bin/go18n merge
// lint go code
// docker run -t --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 10m0s --enable-all -D ireturn -D wrapcheck -D varnamelen -D tagliatelle -D nosnakecase -D gochecknoglobals -D nlreturn -D stylecheck -D lll -D wsl -D scopelint -D varcheck -D exhaustivestruct -D golint -D maligned -D interfacer -D ifshort -D structcheck -D deadcode -D godot -D depguard -D tagalign

View File

@@ -0,0 +1,47 @@
version: "2"
linters:
default: none
enable:
- bodyclose
- canonicalheader
- copyloopvar
- durationcheck
- errcheck
- errchkjson
- errname
- errorlint
- gosec
- govet
- ineffassign
- nilerr
- nilnesserr
- nilnil
- noctx
- rowserrcheck
- sqlclosecheck
- staticcheck
- unconvert
- wastedassign
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gochecknoinits
- gomoddirectives
- wrapcheck
- cyclop
- dupl
- funlen
- gocognit
- gocyclo
- ireturn
- lll
- mnd
- nestif
- nlreturn
- noinlineerr
- paralleltest
- tagliatelle
- testpackage
- varnamelen
- wsl_v5

View File

@@ -51,14 +51,14 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -201,8 +201,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@@ -216,16 +216,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -234,8 +234,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -245,8 +245,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -28,6 +28,7 @@ func (s *service) Load(path string) (*Config, error) {
return nil, merrors.InvalidArgument("config path is required", "path")
}
//nolint:gosec // Configuration file path is provided by service startup configuration.
data, err := os.ReadFile(path)
if err != nil {
s.logger.Error("Failed to read config file", zap.String("path", path), zap.Error(err))

View File

@@ -108,7 +108,7 @@ func (s *service) Start(ctx context.Context) {
if runCtx == nil {
runCtx = context.Background()
}
runCtx, s.cancel = context.WithCancel(runCtx)
runCtx, s.cancel = context.WithCancel(runCtx) //nolint:gosec // canceled by Stop; service lifecycle outlives Start scope
for i := 0; i < s.cfg.WorkerConcurrency; i++ {
workerID := "worker-" + strconv.Itoa(i+1)
@@ -143,6 +143,10 @@ func (s *service) runWorker(ctx context.Context, workerID string) {
now := time.Now().UTC()
task, err := s.tasks.LockNextTask(ctx, now, workerID, s.cfg.LockTTL)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
time.Sleep(s.cfg.WorkerPoll)
continue
}
s.logger.Warn("Failed to lock next task", zap.String("worker_id", workerID), zap.Error(err))
time.Sleep(s.cfg.WorkerPoll)
continue

View File

@@ -106,7 +106,7 @@ func (s *service) Start(ctx context.Context) {
if runCtx == nil {
runCtx = context.Background()
}
runCtx, s.cancel = context.WithCancel(runCtx)
runCtx, s.cancel = context.WithCancel(runCtx) //nolint:gosec // canceled by Stop; service lifecycle outlives Start scope
s.wg.Add(1)
go func() {

View File

@@ -14,6 +14,7 @@ type service struct {
// New creates retry policy service.
func New() Policy {
//nolint:gosec // Backoff jitter is non-cryptographic and only needs pseudo-random distribution.
return &service{rnd: rand.New(rand.NewSource(time.Now().UnixNano()))}
}

View File

@@ -154,6 +154,7 @@ func (i *Imp) Start() error {
runCtx, cancel := context.WithCancel(context.Background())
i.runCancel = cancel
defer cancel()
i.ingest.Start(runCtx)
i.delivery.Start(runCtx)
i.opServer.SetStatus(health.SSRunning)

View File

@@ -379,7 +379,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st
candidates, err := mutil.GetObjects[taskDoc](ctx, r.logger, query, nil, r.repo)
if err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, nil
return nil, merrors.ErrNoData
}
return nil, merrors.InternalWrap(err, "callbacks task query failed")
}
@@ -418,7 +418,7 @@ func (r *taskStore) LockNextTask(ctx context.Context, now time.Time, workerID st
return mapTaskDoc(locked), nil
}
return nil, nil
return nil, merrors.ErrNoData
}
func (r *taskStore) MarkDelivered(ctx context.Context, taskID bson.ObjectID, httpCode int, latency time.Duration, at time.Time) error {

View File

@@ -1,196 +1,47 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
default: none
enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gochecknoinits
- gomoddirectives
- 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: []
- cyclop
- dupl
- funlen
- gocognit
- gocyclo
- ireturn
- lll
- mnd
- nestif
- nlreturn
- noinlineerr
- paralleltest
- tagliatelle
- testpackage
- varnamelen
- wsl_v5

View File

@@ -42,12 +42,12 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -168,8 +168,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
@@ -183,16 +183,16 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -210,8 +210,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -31,6 +31,7 @@ func Load(path string) (*Config, error) {
return nil, merrors.InvalidArgument("config: path is empty")
}
//nolint:gosec // config path is provided by process startup arguments/config.
data, err := os.ReadFile(path)
if err != nil {
return nil, merrors.InternalWrap(err, "config: failed to read file")
@@ -73,8 +74,10 @@ func Load(path string) (*Config, error) {
}
if _, ok := sourceSet[driver]; !ok {
return nil, merrors.InvalidArgument( //nolint:lll
"config: pair references unknown source: "+driver.String(), "pairs."+driver.String())
return nil, merrors.InvalidArgument(
"config: pair references unknown source: "+driver.String(),
"pairs."+driver.String(),
)
}
processed := make([]PairConfig, len(pairList))
@@ -86,14 +89,16 @@ func Load(path string) (*Config, error) {
pair.Symbol = strings.TrimSpace(pair.Symbol)
if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" {
return nil, merrors.InvalidArgument( //nolint:lll
"config: pair entries must define base, quote, and symbol", "pairs."+driver.String())
return nil, merrors.InvalidArgument(
"config: pair entries must define base, quote, and symbol",
"pairs."+driver.String(),
)
}
if strings.TrimSpace(pair.Provider) == "" {
pair.Provider = strings.ToLower(driver.String())
}
processed[idx] = pair
flattened = append(flattened, Pair{
PairConfig: pair,

View File

@@ -14,6 +14,8 @@ import (
"go.uber.org/zap"
)
var errNoSnapshot = errors.New("snapshot not found")
func TestParseDecimal(t *testing.T) {
got, err := parseDecimal("123.456")
if err != nil {
@@ -191,19 +193,9 @@ func (r *ratesStoreStub) UpsertSnapshot(_ context.Context, snapshot *model.RateS
}
func (r *ratesStoreStub) LatestSnapshot(context.Context, model.CurrencyPair, string) (*model.RateSnapshot, error) {
return nil, nil
return nil, errNoSnapshot
}
type repositoryStub struct {
rates storage.RatesStore
}
func (r *repositoryStub) Ping(context.Context) error { return nil }
func (r *repositoryStub) Rates() storage.RatesStore { return r.rates }
func (r *repositoryStub) Quotes() storage.QuotesStore { return nil }
func (r *repositoryStub) Pairs() storage.PairStore { return nil }
func (r *repositoryStub) Currencies() storage.CurrencyStore { return nil }
type connectorStub struct {
id mmarket.Driver
ticker *mmarket.Ticker

View File

@@ -116,7 +116,11 @@ func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmo
return nil, merrors.InternalWrap(err, "binance: request failed")
}
defer resp.Body.Close()
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
c.logger.Warn("Failed to close Binance response body", zap.Error(closeErr))
}
}()
if resp.StatusCode != http.StatusOK {
c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))

View File

@@ -122,9 +122,11 @@ func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Conne
logger,
client,
httpClientOptions{
userAgent: userAgent,
accept: acceptHeader,
referer: referer,
userAgent: userAgent,
accept: acceptHeader,
referer: referer,
allowedScheme: parsed.Scheme,
allowedHost: parsed.Host,
},
),
base: strings.TrimRight(parsed.String(), "/"),
@@ -200,7 +202,11 @@ func (c *cbrConnector) refreshDirectory() error {
)
return merrors.InternalWrap(err, "cbr: directory request failed")
}
defer resp.Body.Close()
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
c.logger.Warn("Failed to close CBR daily response body", zap.Error(closeErr))
}
}()
if resp.StatusCode != http.StatusOK {
c.logger.Warn(
@@ -258,7 +264,11 @@ func (c *cbrConnector) fetchDailyRate(ctx context.Context, valute valuteInfo) (s
)
return "", merrors.InternalWrap(err, "cbr: daily request failed")
}
defer resp.Body.Close()
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
c.logger.Warn("Failed to close CBR historical response body", zap.Error(closeErr))
}
}()
if resp.StatusCode != http.StatusOK {
c.logger.Warn(
@@ -326,7 +336,11 @@ func (c *cbrConnector) fetchHistoricalRate( //nolint:funlen
)
return "", merrors.InternalWrap(err, "cbr: historical request failed")
}
defer resp.Body.Close()
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
c.logger.Warn("Failed to close CBR historical response body", zap.Error(closeErr))
}
}()
if resp.StatusCode != http.StatusOK {
c.logger.Warn(

View File

@@ -2,7 +2,10 @@ package cbr
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/tech/sendico/pkg/mlogger"
@@ -14,17 +17,28 @@ const (
defaultAccept = "application/xml,text/xml;q=0.9,*/*;q=0.8"
)
var (
errNilRequestURL = errors.New("http_client: request URL is nil")
errRelativeRequestURL = errors.New("http_client: request URL must be absolute")
errUnexpectedURLScheme = errors.New("http_client: unexpected URL scheme")
errUnexpectedURLHost = errors.New("http_client: unexpected URL host")
)
// httpClient wraps http.Client to ensure CBR requests always carry required headers.
type httpClient struct {
client *http.Client
headers http.Header
logger mlogger.Logger
client *http.Client
headers http.Header
logger mlogger.Logger
allowedScheme string
allowedHost string
}
type httpClientOptions struct {
userAgent string
accept string
referer string
userAgent string
accept string
referer string
allowedScheme string
allowedHost string
}
func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOptions) *httpClient {
@@ -42,6 +56,20 @@ func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOp
if strings.TrimSpace(referer) == "" {
referer = defaultCBRBaseURL
}
allowedScheme := strings.ToLower(strings.TrimSpace(opts.allowedScheme))
allowedHost := strings.ToLower(strings.TrimSpace(opts.allowedHost))
if allowedScheme == "" || allowedHost == "" {
if parsed, err := url.Parse(referer); err == nil {
if allowedScheme == "" {
allowedScheme = strings.ToLower(parsed.Scheme)
}
if allowedHost == "" {
allowedHost = strings.ToLower(parsed.Host)
}
}
}
httpLogger := logger.Named("http_client")
headers := make(http.Header, 3)
@@ -53,9 +81,11 @@ func newHTTPClient(logger mlogger.Logger, client *http.Client, opts httpClientOp
zap.String("accept", accept), zap.String("referrer", referer))
return &httpClient{
client: client,
headers: headers,
logger: httpLogger,
client: client,
headers: headers,
logger: httpLogger,
allowedScheme: allowedScheme,
allowedHost: allowedHost,
}
}
@@ -74,6 +104,13 @@ func (h *httpClient) Do(req *http.Request) (*http.Response, error) {
enriched.Header.Add(key, value)
}
}
if err := h.validateRequestTarget(enriched.URL); err != nil {
h.logger.Warn("HTTP request blocked by target validation", zap.Error(err), zap.String("method", req.Method))
return nil, err
}
//nolint:gosec // request URL is constrained in validateRequestTarget before any outbound call.
r, err := h.client.Do(enriched)
if err != nil {
h.logger.Warn("HTTP request failed", zap.Error(err), zap.String("method", req.Method),
@@ -85,3 +122,26 @@ func (h *httpClient) Do(req *http.Request) (*http.Response, error) {
func (h *httpClient) headerValue(name string) string {
return h.headers.Get(name)
}
func (h *httpClient) validateRequestTarget(requestURL *url.URL) error {
if requestURL == nil {
return errNilRequestURL
}
if !requestURL.IsAbs() {
return errRelativeRequestURL
}
scheme := strings.ToLower(requestURL.Scheme)
host := strings.ToLower(requestURL.Host)
if h.allowedScheme != "" && scheme != h.allowedScheme {
return fmt.Errorf("%w: %q", errUnexpectedURLScheme, requestURL.Scheme)
}
if h.allowedHost != "" && host != h.allowedHost {
return fmt.Errorf("%w: %q", errUnexpectedURLHost, requestURL.Host)
}
return nil
}

View File

@@ -125,7 +125,11 @@ func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*m
return nil, merrors.InternalWrap(err, "coingecko: request failed")
}
defer resp.Body.Close()
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
c.logger.Warn("Failed to close CoinGecko response body", zap.Error(closeErr))
}
}()
if resp.StatusCode != http.StatusOK {
c.logger.Warn("CoinGecko returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode))

View File

@@ -1,4 +1,5 @@
package common //nolint:revive // package provides shared market connector utilities
// Package common provides shared market connector utilities.
package common
import (
"strconv"

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